Skip to content

指标与可视化:配置指标采集与 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服务。

python
"""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())
json
{
  "ok": 100,
  "error": 10,
  "inflight": 3
}

完整源码、配置与依赖 ​

下列文件与README共用同一份仓库内容,保持输入、失败与清理合同一致。示例未执行;以下命令仅供读者在满足前置后自行运行。

dependencies.txt ​

text
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 ​

yaml
global:
  scrape_interval: 15s
  evaluation_interval: 15s
scrape_configs:
  - job_name: checkout
    static_configs:
      - targets: ['127.0.0.1:9109']
        labels:
          service: checkout

datasources.yml ​

yaml
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: 15s

dashboards.yml ​

yaml
apiVersion: 1
providers:
  - name: m5-next
    orgId: 1
    folder: M5 teaching
    type: file
    disableDeletion: true
    allowUiUpdates: false
    options:
      path: /srv/m5-next-metrics/dashboards

before.prom ​

text
# 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 3

after.prom ​

text
# 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 4

dashboards/checkout.json ​

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 ​

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预期为零;累计值是事件总数,不能当每秒速率。

核对资料与相邻阅读 ​

继续阅读机制、完整实践、运维、诊断与复习。

继续阅读