指标与可视化:配置指标采集与 Grafana 面板
本篇以Prometheus 指标与 Grafana 面板为例;前置是指标类型/单位、抓取窗口与数据源 UID。源码与输入与下文配置共用同一份文件,示例未执行。
核对日期2026-10-17;适用范围:机制按核对时Prometheus 3.x官方文档的稳定共同子集说明;示例另固定Prometheus 2.55与Grafana OSS 11.6教学配置族,归档族不是生产安装建议。新版新增或实验功能未默认启用,实际升级仍需独立兼容与安全评估。示例未执行;真实环境仍需验证产品配置、权限、连通与恢复结果。先读起点理解前置,再按章节完成推演。
四份配置与两个快照
指标配置文件由Prometheus抓取配置、Grafana数据源、dashboard provider和面板JSON组成,依赖声明固定教学版本族。示例未执行;before.prom与after.prom是相隔60秒的人造快照,不是持续运行的Exporter。它们供手算总请求增量和错误比例;读取静态文件不等于Prometheus已完成采样,课程提供未来读者可自跑的只读Exporter完整源码。
数据源UID与服务端地址
指标数据源UID为m5-prometheus,面板引用必须精确一致;名称只负责展示,不能把历史IP:端口数据源名称误当新地址。Grafana通过自身服务进程的网络访问url,不是从读者浏览器直连。若使用容器,需由读者改成容器可达的受控地址并配置信任与鉴权。旧资料的DataSource变量插值需按目标Grafana语法核对,不能照抄旧的$(DataSource)混合写法。
手算期望与配置关系
指标快照推演中200从100增至370、500从10增至40,共增加300次,60秒平均5请求每秒,错误占比30/300为10%。这是独立差值核对,不能冒充真实rate计算,因为rate还处理重置、采样和边界外推。面板按status求rate后汇总,source UID要匹配,provider目录必须由读者替换为真实路径。没有Exporter时预期up=0,不应把空图写成演示成功。
可自跑的只读指标输入源
配套exporter.py和snapshot.json给出完整输入源。程序启动时验证人工快照中ok/error/inflight为非负整数,随后固定监听127.0.0.1:9109;GET /metrics返回Prometheus文本格式,HEAD同状态同长度而无正文,其他路径404,POST/PUT/PATCH/DELETE返回405,未知方法由标准库返回501。抓取不增加业务计数,快照不是性能或流量模型;没有业务变化时rate预期为零。重启载入更小快照可产生Counter reset,真实抓取怎样识别取决于采样窗口,不能靠重启制造可信业务压测。非法JSON/负数在绑定前退出2,端口占用则启动失败;Ctrl+C关闭专用进程。所有行为仅为静态设计,未启动HTTP服务。
"""Optional future self-run fixture exporter; never a real business telemetry source."""
import argparse
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
def load_snapshot(filename):
values = json.loads(filename.read_text(encoding="utf-8"))
required = {"ok", "error", "inflight"}
if set(values) != required or any(type(value) is not int or value < 0 for value in values.values()):
raise ValueError("snapshot requires non-negative integer ok/error/inflight")
return (
"# HELP checkout_requests_total Manually supplied teaching request counts.\n"
"# TYPE checkout_requests_total counter\n"
f'checkout_requests_total{{route="/cart",status="200"}} {values["ok"]}\n'
f'checkout_requests_total{{route="/cart",status="500"}} {values["error"]}\n'
"# HELP checkout_inflight_requests Manually supplied teaching inflight value.\n"
"# TYPE checkout_inflight_requests gauge\n"
f'checkout_inflight_requests {values["inflight"]}\n'
).encode("utf-8")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("snapshot", type=Path)
args = parser.parse_args()
try:
payload = load_snapshot(args.snapshot)
except (OSError, ValueError, TypeError) as error:
print(str(error), file=sys.stderr)
return 2
class Handler(BaseHTTPRequestHandler):
def respond(self, head=False):
body = payload if self.path == "/metrics" else b"not found\n"
self.send_response(200 if self.path == "/metrics" else 404)
self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8" if self.path == "/metrics" else "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
if not head:
self.wfile.write(body)
def do_GET(self):
self.respond()
def do_HEAD(self):
self.respond(head=True)
def deny_write(self):
self.send_response(405)
self.send_header("Allow", "GET, HEAD")
self.send_header("Content-Length", "0")
self.end_headers()
self.close_connection = True
do_POST = deny_write
do_PUT = deny_write
do_PATCH = deny_write
do_DELETE = deny_write
def log_message(self, format, *values):
# Avoid echoing arbitrary untrusted request targets into shared logs.
return
try:
with HTTPServer(("127.0.0.1", 9109), Handler) as server:
server.serve_forever()
except KeyboardInterrupt:
return 0
except OSError as error:
print(str(error), file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main()){
"ok": 100,
"error": 10,
"inflight": 3
}完整源码、配置与依赖
下列文件与README共用同一份仓库内容,保持输入、失败与清理合同一致。示例未执行;以下命令仅供读者在满足前置后自行运行。
dependencies.txt
Prometheus 2.55 configuration/PromQL teaching family (archived, not current recommendation)
Grafana OSS 11.6 dashboard/provisioning teaching family
Python >=3.11,<3.14 standard library for optional future self-run exporter.py; no pip packages.
before.prom and after.prom are snapshots; exporter.py reads a fixed artificial snapshot at startup.prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: checkout
static_configs:
- targets: ['127.0.0.1:9109']
labels:
service: checkoutdatasources.yml
apiVersion: 1
datasources:
- name: M5 Prometheus
uid: m5-prometheus
type: prometheus
access: proxy
url: http://127.0.0.1:9090
isDefault: false
editable: false
jsonData:
httpMethod: POST
timeInterval: 15sdashboards.yml
apiVersion: 1
providers:
- name: m5-next
orgId: 1
folder: M5 teaching
type: file
disableDeletion: true
allowUiUpdates: false
options:
path: /srv/m5-next-metrics/dashboardsbefore.prom
# HELP checkout_requests_total Completed requests.
# TYPE checkout_requests_total counter
checkout_requests_total{route="/cart",status="200"} 100
checkout_requests_total{route="/cart",status="500"} 10
# HELP checkout_inflight_requests Current requests.
# TYPE checkout_inflight_requests gauge
checkout_inflight_requests 3after.prom
# HELP checkout_requests_total Completed requests.
# TYPE checkout_requests_total counter
checkout_requests_total{route="/cart",status="200"} 370
checkout_requests_total{route="/cart",status="500"} 40
# HELP checkout_inflight_requests Current requests.
# TYPE checkout_inflight_requests gauge
checkout_inflight_requests 4dashboards/checkout.json
{
"id": null,
"uid": "m5-checkout",
"title": "M5 Checkout rate",
"schemaVersion": 40,
"version": 1,
"timezone": "utc",
"time": {
"from": "now-15m",
"to": "now"
},
"refresh": "30s",
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Requests per second",
"datasource": {
"type": "prometheus",
"uid": "m5-prometheus"
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 0
},
"targets": [
{
"refId": "A",
"expr": "sum by (status) (rate(checkout_requests_total{job=\"checkout\"}[5m]))",
"legendFormat": "{{status}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps"
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
}
}
}
]
}README.md
# Prometheus 指标与 Grafana 面板完整配置
示例未执行,依赖与版本族见dependencies.txt。prometheus.yml、datasources.yml、dashboards.yml、dashboards/checkout.json完整列出配置;before.prom/after.prom为相隔60秒的人工教学快照,不能当作持续Exporter。
读者前置:自己准备隔离环境中的Prometheus与Grafana;输入源使用本目录完整exporter.py与snapshot.json;本仓库提供将来可自跑的只读Exporter源码,但本轮不启动任何服务。Prometheus目标127.0.0.1:9109仅在同一主机网络命名空间适用。Grafana配置url为Grafana服务进程可达的Prometheus地址;容器中的127.0.0.1指容器自身,必须自行替换。将datasources.yml放入自己的provisioning/datasources,dashboards.yml放入provisioning/dashboards,并把options.path替换为仪表盘JSON的真实绝对目录。不要把整个本仓库挂成可写生产配置。
自跑阅读顺序:先检查Exporter原始指标,再确认Prometheus目标up,最后加载固定UID m5-prometheus的数据源和面板。成功预期:status分组曲线有数据,单位reqps;人工快照200增加270、500增加30,总增量300/60=5请求每秒、错误占比10%。这是简单差值核对,不是Prometheus rate真实算法输出;真实rate处理抓取间隔、重置和窗口边缘外推。
失败预期:目标没有Exporter时up=0;查询窗口只含一个counter样本时rate为空;错误数据源UID使面板找不到数据源;容器误用回环地址导致连接失败。不得用填零掩盖这些失败,也不要把raw URL、用户ID加入label。
恢复清理:先在副本修正数据源连接、时间范围、标签匹配;记录旧JSON和数据源UID后再替换。移除本例专属provisioning文件及m5-checkout面板;disableDeletion=true意味着移除文件不保证删除数据库中的已导入面板,须由读者按Grafana权限显式删除。TSDB目录由读者决定保留,不能为清空图表直接删除生产时间序列。
读者将来可先在此目录自行运行 `python exporter.py snapshot.json`,固定回环9109。启动时只读人工快照,GET /metrics返回HELP/TYPE及三条序列,HEAD同响应头无正文,其他路径404,POST/PUT/PATCH/DELETE为405并给Allow: GET, HEAD,标准库未知方法501;这些HTTP请求不会增加计数。启动期间修改快照不生效,Ctrl+C后重启才重新加载;重启读入更低值会造成counter reset。要比较before/after,读者可以独立快照副本换值并重启,但停机采样与窗口会影响真实rate,不能期望恒定5req/s。常量计数持续采集时rate预期为0。非法/负数输入先退出2,端口占用退出2;程序仅课堂HTTP,禁止公网暴露,未实现生产TLS/鉴权或并发容量。停止只关闭读者自行启动的进程,删除自行创建的快照副本,保留原始fixtures。所有运行结果均未执行。本篇练习与参考解答
练习:固定snapshot持续抓取时QPS应该是快照总计数吗?
参考解答
不是。Counter不变意味着窗口rate预期为零;累计值是事件总数,不能当每秒速率。
核对资料与相邻阅读
继续阅读
- 知识专题
- 知识专题
- 排错与诊断