Cloud Monitoring 仪表盘 Widget 生成:从 PromQL / ListTimeSeries 查询到 SDUI textproto 的三阶段工作流
【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills
本文基于skills/cloud/cloud-monitoring-chart-generation技能(Agent Skill),讲解如何将已解析好的 PromQL 查询或 ListTimeSeries JSON 请求,转换为可直接被 Cloud Monitoring Dashboards API、gcloud CLI 或声明式仪表盘供给流水线消费的google.monitoring.dashboard.v1.WidgetProtocol Buffer textproto。读完本文,你将掌握该技能的三阶段流水线(基线标签计算 → LLM 合成 SemanticPlotSpec → textproto 组装)、两条 API 的互斥约束、UCUM 单位归一化规则,以及配套的 schema 校验与自动重试机制。
技能定位与核心约束
该技能位于 SKILL.md,其元数据声明了明确的适用边界:
- 适用:生成包含
PrometheusQuery或TimeSeriesFilter数据集的合法 Widget textproto;为 Prometheus 或 ListTimeSeries 查询合成 SDUI 组件的标题、坐标轴标签和绘图类型。 - 不适用:指标发现或 PromQL 查询生成本身——这些任务应交给仓库中的
cloud-monitoring-metric-selection和 cloud-monitoring-promql-query 技能。仓库根目录的 index.json 中同样登记了这一分工,可见本技能是整个 Cloud Monitoring 技能族中"查询已就绪、只差落盘成仪表盘组件"的最后一公里。
文档开篇给出了三条必须严格遵守的规则(原文 IMPORTANT 提示框):
- API 偏好:除非用户显式要求 PromQL,或指标数学上必须使用 PromQL,否则始终优先生成
ListTimeSeries(time_series_filter)配置。 - 互斥查询:一个 widget 数据集的
time_series_query中只能包含time_series_filter或prometheus_query二者之一,绝不允许同时填充两个字段。 - 严格透传:必须逐字符拷贝上下文中已提供的 PromQL 查询或 ListTimeSeries JSON filter 字符串,禁止发明、改写或修改查询。
此外还有两条执行纪律(原文 CAUTION 提示框):保持工作目录在 workspace 根目录、禁止cd进入技能子目录;禁止运行任何文件/代码库搜索工具去"发现"指标元数据——指标描述符、查询、单位、资源类型在会话上下文中永远已经存在;直接用 python3 执行随附脚本;assemble_widget_proto会自动生成基于 UUID 的独立文件名以避免并行执行冲突,并以前缀 "Wrote widget textproto to:" 打印到 stderr,后续校验阶段必须从日志中解析这一前缀来定位产物。
环境准备
依赖安装只有一条命令(requirements.txt 声明核心脚本仅使用 Python 3.10+ 标准库模块,运行时无需外部 PyPI 包):
pip install -r scripts/requirements.txt三阶段流水线总览
整个工作流是一条固定管线:
[ Stage 1: compute_labels ] ---> [ Stage 2: LLM Synthesis ] ---> [ Stage 3: assemble_widget_proto ] Generates candidate labels Formulates SemanticPlotSpec Emits validated widget textproto即:脚本先产出候选标题与单位,LLM 基于候选值做语义精修得到SemanticPlotSpec,最后一个脚本把 spec 与查询一起组装成校验通过的 textproto 文件,并由校验器闭环验证。
Stage 1:基线候选合成(compute_labels)
Stage 1 由 compute_labels.py 驱动,根据查询类型分两种调用方式:
# For PromQL: python3 scripts/compute_labels.py \ --metric_display_name "METRIC_DISPLAY_NAME" \ --resource_type "RESOURCE_TYPE" \ --metric_unit "UNIT" \ --promql_query 'PROMQL_QUERY' # For ListTimeSeries: python3 scripts/compute_labels.py \ --metric_display_name "METRIC_DISPLAY_NAME" \ --resource_type "RESOURCE_TYPE" \ --metric_unit "UNIT" \ --filter_string 'metric.type="m"...' \ --per_series_aligner "ALIGN_RATE" \ --cross_series_reducer "REDUCE_SUM"脚本最终向 stdout 打印一个三键 JSON:titleCandidate、yAxisLabelCandidate、unitOverrideCandidate,供 Stage 2 消费。
源码视角:标题是如何合成的
阅读 compute_labels.py 可以发现候选标题并非随意拼接,而是一套有明确约束的模板:
- 资源类型展示名映射:内置
COMMON_RESOURCE_DISPLAY_MAP覆盖 30 余种标准受监控资源,如gce_instance→ "VM Instance"、k8s_pod→ "Kubernetes Pod"、spanner_instance→ "Cloud Spanner Instance"、gcs_bucket→ "GCS Bucket" 等。当指标名存在歧义时(is_ambiguous_metric_name为真),标题会以 "VM Instance - CPU Utilization" 这种"资源 - 指标"格式前缀化。 - 筛选与分组:从 PromQL 的标签匹配器或 LTS filter 字符串中提取等值条件(剔除
project_id、resource.type等噪声键),标题追加for <前两个筛选值>;group by (...)字段会被清洗掉metric.label./resource.前缀后追加by <字段>。 - 聚合标注:聚合方式(如
RATE、SUM)以大写[AGG]后缀呈现。compute_labels_test.py 中有对应断言:输入 "Disk Read Bytes" +gce_instance+ zone 筛选 +device_name分组 +SUM聚合,产出精确等于"Disk Read Bytes for us-east1-d by device_name [SUM]"。 - 80 字符硬约束:超过 80 字符时先压缩(过长的 group 部分替换为 " (grouped)"、筛选部分替换为 " (filtered)"),仍超长则截断至 77 字符并补 "..."。测试
test_compute_widget_title_long_truncation验证了截断结果必然endswith("...")且长度不超过 80。
源码视角:PromQL 特征提取
对 PromQL 分支,extract_promql_features会先做三步净化——移除字符串字面量(双引号/单引号/反引号)、移除模板变量(${...}、$var、[[...]])、移除标签匹配器块{...}——然后再做:
- 用
\b(rate|irate)\s*\(检测速率函数; - 在净化后的查询中查找第一个聚合关键字(
sum/avg/count/min/max/stddev/stdvar/topk/bottomk/count_values/quantile),并刻意跳过compute_googleapis_com:这类带:、.、/的指标标识符片段,避免把命名空间误判为聚合函数; - 用
\bby\s*\(([^)]+)\)提取分组字段。
对 LTS 分支,extract_filter_features从 filter 字符串中解析metric.type、resource.type和其余等值条件,且has_rate直接由per_series_aligner == "ALIGN_RATE"判定——也就是说 LTS 流程中"是否速率"完全由对齐器表达。
源码视角:UCUM 单位归一化
normalize_ucum_unit实现了文档中 "LTS 单位策略" 背后的数学处理:
- 空单位或哨兵值
{not_a_unit}→ 空字符串; - 去除空白,把字面量
(rate)替换为/s; - 若存在 rate aligner 且单位尚未以
/s结尾,则自动追加/s(By→By/s,1→1/s); - 剥离
{...}形式的维度标注,例如s{CPU}/s归一化为s/s; 10^2.%统一归一化为%。
归一化结果再查CANONICAL_UNIT_DISPLAY_MAP得到坐标轴展示名:%/10^2.%→ "Utilization",By→ "Bytes",By/s→ "Bytes Rate",s/s→ "Utilization",1/s→ "Operations Rate" 等。compute_axis_label在单位集合唯一且非空时直接返回该展示名;多指标单位不一致时回退为指标展示名列表(用逗号连接);完全无单位时返回兜底值 "PromQL Metric Axis"。测试文件 compute_labels_test.py 覆盖了10^2.%→ "Utilization"、By+ rate → "Bytes Rate"、多单位回退到 "Disk Read Bytes, Read Latency" 等关键路径。
这正是 SKILL.md 中 "Trust the Candidate" 策略的依据:LTS 流程下unitOverrideCandidate已经是数学处理后的结果,Stage 2 直接照抄即可。
Stage 2:SemanticPlotSpec 合成(LLM)
审阅用户提示词、查询结构和 Stage 1 候选值后,LLM 需要产出一个四键JSON 对象SemanticPlotSpec:
title:在titleCandidate基础上润色,保证简洁、人类可读、且不超过 80 字符;yAxisLabel:简洁的定量描述词或指标概念,如"Utilization"、"Bytes"、"Bytes Rate"。不要在标签后追加单位符号或后缀(如"(%)"、"(/s)"、"(By)"),因为单位会经由unitOverride自动渲染;plotType:默认LINE;用户要求或分布类查询时使用STACKED_AREA;unitOverride:设为 UCUM 单位字符串,按以下两套策略推导。
LTS 单位策略
直接采用 Stage 1 产出的unitOverrideCandidate。Stage 1 会数学化处理ALIGN_RATE(例如输出By/s)、对ALIGN_PERCENT_CHANGE强制输出%,并无条件正确输出原生归一化结果——compute_labels.py 中per_series_aligner == "ALIGN_PERCENT_CHANGE"时unit_override被硬性覆盖为"%",与此说明一一对应。
PromQL 单位策略(LLM 手动推导)
由于 PromQL 表达式可以几何级组合(例如histogram_quantile(..., rate(...))),最终单位必须由 LLM 的语义推理决定:
- 速率函数(
rate(...)、irate(...)):把累积计数器转为每秒速率,在原始指标单位后追加/s。例如原始单位为By且套了rate(...),则unitOverride: "By/s"。- 例外:若
rate()出现在histogram_quantile()内部,输出是原始桶单位(如"s"),而不是速率。
- 例外:若
- 比率与百分比(
100 * (A / B)):相同单位的比率通常表示百分比,unitOverride: "%"。 - 归一化:
10^2.%归一化为"%"。 - 保留单位:简单的聚合函数(如
avg_over_time(...)、sum by (...))保持底层指标单位不变。
文档还特别强调:不要配置legend_template字段——它被刻意省略,以便 Cloud Monitoring 前端在运行时动态渲染其多列表格图例。这一点在源码中同样有体现:assemble_widget_proto.py 的assemble_widget_textproto不输出任何legend_template行,且 assemble_widget_proto_test.py 用assertNotIn("legend_template:", proto_text)做了硬断言。
文档给出的示例 spec:
{ "title": "VM CPU Utilization us-central1-a", "yAxisLabel": "Utilization", "plotType": "LINE", "unitOverride": "%" }Stage 3:Protobuf 组装与输出(assemble_widget_proto)
Stage 3 由 assemble_widget_proto.py 执行,按查询类型二选一:
# For PromQL: python3 scripts/assemble_widget_proto.py \ --promql_query 'PROMQL_QUERY' \ --spec_json 'SEMANTIC_PLOT_SPEC_JSON' # For ListTimeSeries: python3 scripts/assemble_widget_proto.py \ --lts_request_json '{"filter": "...", "aggregation": {...}}' \ --spec_json 'SEMANTIC_PLOT_SPEC_JSON'文档在此处的文件输出契约(MANDATORY FILE OUTPUT CONTRACT)要求:不要猜测或强制指定输出文件名。脚本自动生成保证唯一性的文件名,并把路径打印到 stderr;需从 stderr 中搜索前缀 "Wrote widget textproto to:" 确定性地捕获该文件名,再作为 Stage 4 校验的目标。源码印证了这一契约:get_auto_output_path以uuid.uuid4().hex[:8]生成chart_<8位hex>.textproto并循环检查避免撞名,main结尾执行print(f"Wrote widget textproto to: {output_path}", file=sys.stderr)(assemble_widget_proto.py、#L272-L277)。测试test_get_auto_output_path断言产物必然以chart_开头、以.textproto结尾。
--spec_json中的四个键会覆盖命令行上的--title/--plot_type/--y_axis_label/--unit_override;LTS 分支的--lts_request_json是必填filter键的 JSON,缺失时脚本以退出码 1 报错。最终在聊天回复中,生成的 textproto 应包裹在```textproto代码块中呈现:
title: "..." xy_chart { ... }源码视角:textproto 的精确结构与合法值白名单
assemble_widget_textproto生成的结构完全固定:widget { title, xy_chart { chart_options { mode: COLOR }, data_sets { time_series_query, plot_type, target_axis: Y1 }, y_axis { label, scale } } }。其中几个校验点值得注意:
- 绘图类型白名单:只接受
LINE、STACKED_AREA、STACKED_BAR、HEATMAP,非法值静默回退为LINE(assemble_widget_proto.py); - 对齐器/归约器白名单:
perSeriesAligner必须在 19 个VALID_ALIGNERS内(ALIGN_NONE、ALIGN_DELTA、ALIGN_RATE、ALIGN_INTERPOLATE、ALIGN_NEXT_OLDER、ALIGN_MIN/MAX/MEAN/COUNT/SUM/STDDEV、ALIGN_COUNT_TRUE/FALSE、ALIGN_FRACTION_TRUE、ALIGN_PERCENTILE_99/95/50/05、ALIGN_PERCENT_CHANGE),crossSeriesReducer必须在 14 个VALID_REDUCERS内(REDUCE_NONE至REDUCE_PERCENTILE_05),任一非法都抛出ValueError并以退出码 1 终止(#L9-L46); - 时长解析:
aggregation.alignmentPeriod支持s/m/h/d后缀的健壮解析(如60s→seconds: 60); - 字符串转义:
format_proto_string对\和"做 protobuf 文本格式转义,保证查询中内嵌引号(常见于 LTS filter 的metric.type="...")不会被破坏。
以一个典型的 ListTimeSeries CPU 利用率 widget 为例,组装结果形如:
widget { title: "GCE Instance CPU Utilization" xy_chart { chart_options { mode: COLOR } data_sets { time_series_query { time_series_filter { filter: "metric.type=\"compute.googleapis.com/instance/cpu/utilization\"" aggregation { alignment_period { seconds: 60 } per_series_aligner: ALIGN_MEAN cross_series_reducer: REDUCE_NONE } } unit_override: "%" } plot_type: LINE target_axis: Y1 } y_axis { scale: LINEAR } } }而 validate_chart_test.py 中的真实样例展示了 PromQL 分支的产物形态:
widget { title: "GCE Instance CPU Utilization" xy_chart { chart_options { mode: COLOR } data_sets { time_series_query { prometheus_query: "100 * avg(compute_googleapis_com:instance_cpu_utilization)" unit_override: "%" } plot_type: LINE target_axis: Y1 } y_axis { label: "Utilization (%)" scale: LINEAR } } }注意两个分支的差异:LTS 分支输出time_series_filter { filter + aggregation },PromQL 分支输出单行prometheus_query: "..."——二者在结构上天然互斥,与文档 IMPORTANT 框的第 2 条规则严格一致。
校验与自动重试(validate_chart)
文档将"校验通过"设为结束回合的前置条件(DO NOT FINISH YOUR TURN UNTIL FILE VERIFICATION PASSES),流程为:
- 验证产物:对 Stage 3 生成的文件运行校验器,PromQL 与 LTS 图表分别用不同子串参数:
# For PromQL charts: python3 scripts/validate_chart.py --input_file "GENERATED_FILE.textproto" \ --expected_promql_substring "SOME_IDENTIFYING_SUBSTRING_FROM_QUERY" \ --expected_unit_override "UNIT_OVERRIDE_CANDIDATE" # For ListTimeSeries (LTS) charts: python3 scripts/validate_chart.py --input_file "GENERATED_FILE.textproto" \ --expected_lts_filter_substring "SOME_IDENTIFYING_SUBSTRING_FROM_FILTER" \ --expected_unit_override "UNIT_OVERRIDE_CANDIDATE"必须始终提供识别子串和 Stage 1 的单位候选值,以确认数据未被篡改。为多个指标生成多张图表时,必须对每个文件独立运行一次校验。
缺失或失败时自动重试:若
validate_chart报告文件缺失或非法,核对参数后立即重跑 Stage 3。重试上限:因 schema 或语法错误导致的校验失败,修正参数后最多重试 2 次;2 次后仍失败则停止重试,向用户报告校验错误并给出尽力而为的 textproto。
区分错误类型:
validate_chart.py的 schema/语法校验错误与环境/沙箱执行限制是两类问题,后者走下面的"优雅回退"流程。
validate_chart.py 的验证逻辑与文档描述逐项对应:
- 结构校验(Phase 1):标题非空且不超过 80 字符;必须存在
xy_chart且至少一个data_set;每个data_set必须恰好含prometheus_query或time_series_filter之一(两者皆无或皆有都会抛错——测试test_validate_widget_dual_query_fails专门断言同时填充两者时报 "cannot contain BOTH")。 - 断言校验(Phase 2):在任一
data_set上匹配--expected_promql_substring/--expected_lts_filter_substring/--expected_unit_override/--expected_plot_type(注意 LTS 子串匹配的是time_series_filter.filter字段)。 - 输入来源:
--input_file支持具体路径、-(STDIN)或留空(glob 工作目录下全部*.textproto)。
底层解析器:零依赖的 textproto 语法校验
校验依赖 textproto_util.py,它不依赖 protobuf 运行时,而是实现了一个轻量级的基于语法的解析器:
tokenize_textproto把文本切成SYM/STR/{/}/:token,支持注释行、单双引号字符串和反斜杠转义;parse_textproto_tokens递归地把 token 流解析为嵌套字典(重复键自动转列表,符合 textproto 的 repeated 语义);dict_to_widget映射到Widget → XyChart → DataSet → TimeSeriesQuery → TimeSeriesFilter/Aggregation的 dataclass 层次,并做严格字段边界检查——任何一层出现未知键(如顶层多了字段、aggregation里冒出alignment_period之外的键)都会抛出ValueError。这意味着校验器不只是"能解析",还保证产物不会携带 Dashboards API 语义之外的多余字段。
validate_chart_test.py 覆盖了带widget { }外层包裹、不带包裹、空标题失败、双查询失败、STDIN 输入(--input_file -配合rate(compute_googleapis_com:instance_disk_read_bytes_count[1m])+unit_override: "By/s"断言)等场景,与文档的 Stage 4 行为闭环。
沙箱优雅回退
当compute_labels.py、assemble_widget_proto.py或validate_chart.py因环境或沙箱限制无法执行时,文档要求:
- 告知用户哪个脚本无法执行及原因;
- 在回复中直接合成并输出完整的 widget textproto,遵循全部格式与单位规则;
- 附一个"Local Verification"小节,包含独立的 python3 命令,方便用户本地运行校验 schema。
与相邻技能的衔接关系
在本仓库的技能族中,cloud-monitoring-chart-generation处于明确的流水线末端:指标选择由 cloud-monitoring-metric-selection 负责,ListTimeSeries 请求体构造由 cloud-monitoring-list-time-series-request 负责,PromQL 表达式编写由 cloud-monitoring-promql-query 负责;这些技能产出的"已解析查询 + 指标元数据"(display name、resource type、unit)正是本技能 Stage 1 的四个必填输入。文档的 "No Discovery Or Search Rule" 之所以成立,正是因为上游技能已经完成了发现工作——这也是理解该技能执行纪律的关键。
小结
该技能把"把一条已就绪的监控查询变成合法仪表盘组件"这件事拆成了三个职责单一的环节:确定性脚本负责候选标签与单位数学(可被 compute_labels_test.py 等测试精确断言)、LLM 负责语义层的标题润色与 PromQL 单位推理、确定性脚本负责 proto 组装与白名单校验(plot_type、aligner、reducer),最后由零依赖的 textproto 解析器做闭环验证。配合 UUID 文件名、stderr 前缀契约和"最多重试 2 次"的止损策略,整条管线既适合单指标手工操作,也适合在并行会话中批量生成多个 widget 文件。
配套参考(文档 Supporting Links 指向的官方资源):Cloud Monitoring Dashboards API 文档与 Prometheus 查询语言文档;仓库内可进一步阅读 SKILL.md 与 scripts/ 目录下的完整实现与测试。
【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考