告警注解与通知模板主题封面,展示观察、判断与行动三个步骤。

注解解释告警现场,同时保留稳定身份

告警标签标识正在跟踪哪一个条件,并提供路由与分组字段;注解则解释这个条件,例如当前值、简短摘要或操作手册链接。把变化中的说明文字放进标签,可能把同一个持续问题拆成多个身份;反过来,如果责任归属字段只存在于注解中,依赖标签的路由又可能无法识别应该由谁处理。两类字段都重要,但承担不同职责。 区分身份与当前说明 队列告警可以保持 alertname、service 与 instance 标签稳定,同时把摘要从等待六项更新为等待八项。Alertmanager 可以在相同告警指纹下更新注解,而不是新增一条告警。指纹来自身份标签,不是通知渲染文本的哈希,所以文字改变不应该被直接理解为发生了另一个独立事件。这个边界也方便操作人员跟踪问题的持续变化。 该区别在进入 Alertmanager 之前也有影响。Prometheus 按告警标签集跟踪等待时长,如果标签中包含不断变化的当前值,就可能持续创建新身份,导致预期等待过程无法完成。除非数值确实定义另一种告警条件,否则应把它放在注解中。遇到反复等待却不触发的问题,应同时审阅规则标签和模板内容,而不是只调整等待参数。 一组告警未必拥有共同摘要 通知模板能访问多个层级的数据。GroupLabels 包含分组选择的标签,CommonLabels 包含通知中所有告警都具有的相同标签对,CommonAnnotations 同样只包含组内共有的注解。Alerts 列表则保留各成员自己的标签和注解,因此可以逐条解释不同实例的现场情况。共同字段与全部字段不是同一概念,字段存在于某条告警中也不保证它存在于组级对象中。 两个副本的摘要描述不同队列长度时,CommonAnnotations 就没有一个共同摘要。只打印该字段的模板,可能恰好省掉响应人员最需要的信息。更合理的方式是用共同上下文组织标题,再遍历每条告警输出变化中的说明。即使当前值不同,两条告警仍可能共享同一份操作手册链接,这个链接就可以继续作为共同注解使用。 也不能假定每一组都具有实例这样的分组标签。按服务与告警名分组,本来就是为了合并不同实例。模板应对应真实分组契约,并主动处理缺少共同字段的情况。代表性输入需要覆盖多条告警、不同注解,以及相关触发或恢复状态,不能只用一条理想告警验证,因为单条输入很容易让所有字段都碰巧成为共同字段。 坏掉的样子 动态标签会让每次数值变化产生新指纹,增加告警变化量,并可能重新开始等待。匹配 team 的路由不能用同名注解代替标签;分组策略与模板假设不一致时,通知标题也可能只描述一台实例,正文却混入不同服务的告警。应把身份、分组与展示作为一条真实调用链检查,避免各部分单独看起来合理,组合后却误导读者。 模板语法通过,并不保证真实分组数据能渲染出完整且准确的消息。应核对渲染内容,而不是只检查解析成功。反过来,渲染成功也不能证明通知集成接受该消息,或消息已经送达。凭据与敏感请求详情不应放在注解中,因为这些文字可能被复制到路由选择的所有接收器,也可能进入后续通知记录。 快速验证 把程序保存为 annotations.py,在 Docker 可用时运行 python3 annotations.py。使用 Alertmanager 0.28.1,必要时下载镜像。隔离服务器的接收器没有实际发送集成;程序通过真实接口提交两条合成告警,然后在标签完全不变的情况下更新其中一条注解。操作只发生在本次临时服务器中,不会修改已有告警状态。 import http.client import io import json import subprocess import time import uuid name = "xbcatm-monitoring-" + uuid.uuid4().hex[:12] class ResponseSocket: def __init__(self, data): self.data = data def makefile(self, mode): return io.BytesIO(self.data) def request(method, path, body="", content_type="application/json"): body = body.encode() headers = (f"{method} {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n" f"Connection: close\r\nContent-Type: {content_type}\r\n" f"Content-Length: {len(body)}\r\n\r\n").encode() result = subprocess.run( ["docker", "exec", "-i", name, "/bin/nc", "-w", "3", "127.0.0.1", str(port)], input=headers + body, capture_output=True, timeout=5, ) if result.returncode: raise RuntimeError(result.stderr.decode()) response = http.client.HTTPResponse(ResponseSocket(result.stdout)) response.begin() return response.status, response.read().decode() def get_json(path): status, body = request("GET", path) assert status == 200, (status, body) return json.loads(body) def eventually(check): for attempt in range(150): try: value = check() if value: return value except (RuntimeError, AssertionError, http.client.HTTPException): pass time.sleep(0.1) raise AssertionError("Expected local server state did not appear") port = 9093 script = r"""set -eu cat > /tmp/alertmanager.yml <<'ALERT_CONFIG' route: receiver: preview group_by: [alertname, service] group_wait: 1h group_interval: 5m repeat_interval: 4h receivers: - name: preview ALERT_CONFIG exec /bin/alertmanager --config.file=/tmp/alertmanager.yml --storage.path=/tmp/data \ --web.listen-address=127.0.0.1:9093 --cluster.listen-address= """ try: subprocess.run( ["docker", "run", "-d", "--rm", "--name", name, "--network", "none", "--entrypoint", "/bin/sh", "prom/alertmanager:v0.28.1", "-c", script], check=True, capture_output=True, text=True, timeout=20, ) eventually(lambda: request("GET", "/-/ready")[0] == 200) alerts = [{"labels": {"alertname": "QueueHigh", "service": "checkout", "instance": instance}, "annotations": {"summary": "queue=" + str(depth), "runbook": "https://example.invalid/queue"}} for instance, depth in [("api-a", 6), ("api-b", 9)]] assert request("POST", "/api/v2/alerts", json.dumps(alerts))[0] == 200 before = eventually(lambda: (a if len(a := get_json("/api/v2/alerts")) == 2 else False)) fingerprints = {a["labels"]["instance"]: a["fingerprint"] for a in before} alerts[0]["annotations"]["summary"] = "queue=8" assert request("POST", "/api/v2/alerts", json.dumps([alerts[0]]))[0] == 200 def updated_annotations(): items = get_json("/api/v2/alerts") changed = any(x["labels"]["instance"] == "api-a" and x["annotations"]["summary"] == "queue=8" for x in items) return items if changed else None updated = eventually(updated_annotations) assert len(updated) == 2 assert {a["labels"]["instance"]: a["fingerprint"] for a in updated} == fingerprints # Supply the documented notification Data shape to the real template renderer. data = {"receiver": "preview", "status": "firing", "groupLabels": {"alertname": "QueueHigh", "service": "checkout"}, "commonLabels": {"alertname": "QueueHigh", "service": "checkout"}, "commonAnnotations": {"runbook": "https://example.invalid/queue"}, "alerts": [{"status": "firing", "labels": a["labels"], "annotations": a["annotations"]} for a in sorted(updated, key=lambda a: a["labels"]["instance"])]} template = '{{define "preview"}}{{.GroupLabels.service}}|common-summary={{with .CommonAnnotations.summary}}{{.}}{{else}}none{{end}}{{range .Alerts}}\n{{.Labels.instance}}:{{.Annotations.summary}}{{end}}{{end}}' for path, content in [("/tmp/preview.tmpl", template), ("/tmp/data.json", json.dumps(data))]: subprocess.run(["docker", "exec", "-i", name, "/bin/sh", "-c", "cat > " + path], input=content, text=True, capture_output=True, check=True, timeout=5) rendered = subprocess.run(["docker", "exec", name, "/bin/amtool", "template", "render", "--template.glob=/tmp/preview.tmpl", "--template.data=/tmp/data.json", '--template.text={{ template "preview" . }}'], text=True, capture_output=True, check=True, timeout=5).stdout.strip() assert rendered == "checkout|common-summary=none\napi-a:queue=8\napi-b:queue=9", rendered print(json.dumps({"alert_count": 2, "fingerprints_unchanged": True, "rendered": rendered}, indent=2)) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 结果确认更新后仍只有两条告警,指纹也保持不变。程序随后向真实 amtool 模板渲染器提供文档规定的数据结构:共同注解故意只保留共享手册链接,不放入彼此不同的摘要。这里明确提供的输入验证渲染行为,不代表已验证服务器如何组装实际通知,更不能替代发送链路测试。 ...

二月 22, 2028 · 3 分钟 · XB Field Notes
周期执行的记录规则主题封面,展示观察、判断与行动三个步骤。

记录规则保存按周期计算的查询结果

记录规则在指定评估时刻执行 PromQL 表达式,把结果作为一个新指标保存。反复刷新的面板可以读取这个指标,减少重复计算。但简单把它称为缓存,会遮住一个重要区别:记录序列有自己的样本历史、写入节奏和标签集合,并不是任意查询请求的透明缓存。使用者必须知道读到的是哪次预计算,以及这个结果保留了哪些信息。 三个时钟共同影响页面上的值 抓取决定原始观测何时进入存储,规则评估决定表达式何时生成记录样本,面板则决定何时以及以什么步长查询两者。把页面刷新得更快,不会让记录规则提前计算。因此,在两次规则评估之间读取记录指标,可能得到比同一时刻直接执行原表达式更旧的值。这是不同采样节奏的正常结果,不能仅凭两条曲线不同便断定计算错误。 规则组可以指定周期;未单独指定时,采用全局规则评估周期。同组规则按顺序执行,并使用相同的评估时间戳,所以后面的规则可以使用前面规则刚生成的结果。独立规则组即使写了相同的周期,也不能因此推导存在同样的先后顺序。依赖关系需要明确设计,不能依靠某次运行中碰巧出现的执行顺序。 预计算也改变了保留的数据 聚合表达式决定输出保留哪些维度。如果规则把 instance 求和消掉,面板之后就无法仅从这条记录序列恢复实例分布。仍需实例级排查时,应保留原始指标,并围绕已知消费者选择记录维度。每个剩余标签组合又是一条需要存储的新序列,因此把所有可能的聚合都做成规则,可能显著增加写入量和存储量,并不总是节省资源。 修改表达式会影响后续评估。正常重载规则不会用新定义重新计算全部旧样本,也不会自动填补规则创建之前的历史。在相同指标名下改变含义,可能让同一张图同时包含两套不兼容口径。应记录定义和时间窗口,在消费者要求历史可比时安排明确迁移,不能把正常重载解释为历史数据已经整体更新。 坏掉的样子 面板看起来停住,但原始样本仍在进入数据库时,应对照记录序列最近样本、规则组最近评估、执行耗时与错误。如果上一轮还没结束,下一次计划评估到来时会被跳过,直到当前轮完成或超时。把昂贵查询变成记录规则,只是把计算移入调度器,没有让计算成本消失。执行耗时长期接近周期时,新鲜度会比配置值更差。 查询变快也可能是因为聚合后回答的问题更窄。庆祝速度提升前,应确认被移除的维度确实不再需要。简单表达式通常不值得只为性能增加记录规则。应先测量重复查询成本和复用频率,再把新增序列、周期执行以及新鲜度延迟一起纳入判断。面板数量很多,也不等于每个表达式都必须预先保存。 快速验证 保存程序为 recording-cadence.py,执行 python3 recording-cadence.py。环境需要 Docker 与公开的 Prometheus 3.5.1 镜像,镜像缺失时可以由 Docker 获取。真实 promtool 引擎接收每分钟一份的队列值,而单元测试评估步长和规则组周期均设为两分钟,明确把原始观测与预计算节奏分开。测试数据时间不等于实际等待两分钟。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 2m rule_files: - /tmp/rules.yml tests: - interval: 1m input_series: - series: "queue_depth{queue=\"orders\"}" values: "1 4 9 16" promql_expr_test: - expr: "sum(queue_depth)" eval_time: 1m exp_samples: - labels: "{}" value: 4 - expr: "queue:depth:sum" eval_time: 1m exp_samples: - labels: "{__name__=\"queue:depth:sum\"}" value: 1 - expr: "queue:depth:sum" eval_time: 2m exp_samples: - labels: "{__name__=\"queue:depth:sum\"}" value: 9 """ rules = r"""groups: - name: slow-recording interval: 2m rules: - record: queue:depth:sum expr: sum(queue_depth) """ script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 预期输出为 SUCCESS。第一分钟原始求和结果是四,但记录值仍为第零分钟的一;第二分钟记录序列更新为九。这里故意选择很便宜的表达式,以隔离周期差异,不是在建议为每个队列仪表值建立记录规则,也没有测量真实调度器在负载下的性能。输入时间线和断言直接说明为何同一时刻两个查询可以返回不同结果。 ...

一月 26, 2028 · 1 分钟 · XB Field Notes
指标标签基数主题封面,展示观察、判断与行动三个步骤。

高基数标签在查询之前就增加存储序列

Prometheus 序列由指标名与完整标签集合识别。在计数器上增加客户或请求身份,即使所有面板都会立刻把这些身份求和,也仍可能创建大量序列。查询聚合改变返回结果,不会删除已经入库的原始序列。存储和写入成本在用户打开面板之前就已经产生,因此不能只看面板返回多少行来判断仪表化是否足够节制。 统计实际组合,而不是孤立取值 一个新的标签值,不是在整个数据库中自动对应一条新序列。它在指标名及其他标签形成不同组合的位置上,分别构成新身份。在明确指标与目标范围内,可能取值数的乘积给出笛卡尔组合上界;只有每种组合实际出现时,它才等于观测数量。维度之间存在约束时,上界可能远大于现实。真实数量还受到目标标签与指标族形态影响,不能把理论乘积直接宣称为已经发生的序列数量。 来自少量路由模板的标签可以有边界,包含标识符和查询参数的完整 URL 往往没有。客户身份、请求编号、任意错误文本和时间戳也需要明确审视。带多个桶序列的直方图,还会进一步放大每组标签组合的影响。预算应依据真实输出和预期工作负载,而不是只看某一个维度有多少取值,也不能因为每条样本数值很小便忽略身份成本。 查询少几行,不代表少存几行 按路由求和的表达式,可以读取十二条客户级序列后只返回三个结果,但十二条输入仍然存在。如果指标契约需要的就是路由总量,仪表化可以直接维护路由级计数器,只导出三条序列。这是在源头减少输出身份,不是要求每个消费者都重复隐藏额外维度。两种方式可能显示相同总数,却具有不同的数据保留和成本含义。 入库时删除标签不等于完成上述累计。不同样本可能获得同一个身份,而重新标记不会替它们决定如何相加。应使用明确的仪表化变更,或定义清楚的记录查询;后者会新增派生序列,原始数据仍按照存储策略保留。确实需要详细关联时,可以放入日志或追踪,但它们同样需要各自的权限、保留期限与成本约束,不是完全没有代价的替代存储。 使用正确范围的清单诊断增长 TSDB 状态接口提供头部统计,其中包括按指标名汇总的序列数量。这些值有助于定位大型指标族,但不是完整成本模型。采样间隔、标签长度、身份更替、数据块处理、保留策略和存储行为都会影响资源。不能把一个小实验中减少的序列比例,直接换算成生产内存或磁盘会减少的相同比例,更不能把该比例当成容量压测结果。 当一些头部序列已经没有近期可选择样本时,即时查询计数与头部序列统计可能不同;历史存储块又是另一种范围。比较报告时应记录测量时刻和统计总体。某次发布改变标签值后,旧身份仍可能留在存储中,因此查询当前返回更少结果,不代表之前的全部成本都已经消失。需要分别观察新增速度、当前可见序列和保留历史。 坏掉的样子 新增无边界标签可能持续增加写入与内存压力,直到抓取或查询受到影响。应先定位指标族与仪表化改动,再从源头或经过审阅的样本过滤策略阻止不必要的新身份。立即删除历史数据不是所有场景都必须采用的恢复步骤,改变保留时间也不会瞬间清除全部资源成本。处理方式应依据实际瓶颈,而不是套用统一删除命令。 更隐蔽的失败是查询聚合很成功,面板看起来很小,但原始指标仍持续增长。应同时审阅导出身份和查询输出,为新增维度说明预期取值范围、有用消费者与负责人。生产计数器速率查询还应保留正确的重置处理;本实验用于比较固定总量的即时求和,不能代替先求速率再聚合的运行监控语义。 快速验证 保存程序为 cardinality.py,执行 python3 cardinality.py。需要 Docker 与 Prometheus 3.5.1,必要时下载镜像。本地文本端点为三个路由分别导出四个合成客户身份,同时独立导出已经累计好的路由总量。真实服务器抓取这些样本,并提供查询结果与 TSDB 统计,实验不会使用任何真实客户信息。 import http.client import io import json import subprocess import time import uuid from urllib.parse import quote name = "xbcatm-monitoring-" + uuid.uuid4().hex[:12] class ResponseSocket: def __init__(self, data): self.data = data def makefile(self, mode): return io.BytesIO(self.data) def request(method, path, body="", content_type="application/json"): body = body.encode() headers = (f"{method} {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n" f"Connection: close\r\nContent-Type: {content_type}\r\n" f"Content-Length: {len(body)}\r\n\r\n").encode() result = subprocess.run( ["docker", "exec", "-i", name, "/bin/nc", "-w", "3", "127.0.0.1", str(port)], input=headers + body, capture_output=True, timeout=5, ) if result.returncode: raise RuntimeError(result.stderr.decode()) response = http.client.HTTPResponse(ResponseSocket(result.stdout)) response.begin() return response.status, response.read().decode() def get_json(path): status, body = request("GET", path) assert status == 200, (status, body) return json.loads(body) def eventually(check): for attempt in range(150): try: value = check() if value: return value except (RuntimeError, AssertionError, http.client.HTTPException): pass time.sleep(0.1) raise AssertionError("Expected local server state did not appear") def query(expression): response = get_json("/api/v1/query?query=" + quote(expression, safe="")) assert response["status"] == "success" return response["data"]["result"] def value(expression): result = query(expression) assert len(result) == 1, (expression, result) return float(result[0]["value"][1]) port = 9090 # Requires Docker and the Prometheus 3.5.1 image. script = r"""set -eu mkdir -p /tmp/www /tmp/targets cat > /tmp/prom.yml <<'PROM_CONFIG' global: scrape_interval: 1s scrape_timeout: 500ms scrape_configs: - job_name: cardinality metrics_path: /metrics.txt static_configs: - targets: ['127.0.0.1:8080'] PROM_CONFIG cat > /tmp/www/metrics.txt <<'METRIC_DATA' # TYPE raw_requests_total counter # TYPE bounded_requests_total counter raw_requests_total{route="/orders",customer_id="synthetic-1"} 1 raw_requests_total{route="/orders",customer_id="synthetic-2"} 2 raw_requests_total{route="/orders",customer_id="synthetic-3"} 3 raw_requests_total{route="/orders",customer_id="synthetic-4"} 4 bounded_requests_total{route="/orders"} 10 raw_requests_total{route="/cart",customer_id="synthetic-1"} 1 raw_requests_total{route="/cart",customer_id="synthetic-2"} 2 raw_requests_total{route="/cart",customer_id="synthetic-3"} 3 raw_requests_total{route="/cart",customer_id="synthetic-4"} 4 bounded_requests_total{route="/cart"} 10 raw_requests_total{route="/search",customer_id="synthetic-1"} 1 raw_requests_total{route="/search",customer_id="synthetic-2"} 2 raw_requests_total{route="/search",customer_id="synthetic-3"} 3 raw_requests_total{route="/search",customer_id="synthetic-4"} 4 bounded_requests_total{route="/search"} 10 METRIC_DATA /bin/httpd -f -p 127.0.0.1:8080 -h /tmp/www > /tmp/httpd.log 2>&1 & exec /bin/prometheus --config.file=/tmp/prom.yml --storage.tsdb.path=/tmp/data --web.listen-address=127.0.0.1:9090 """ try: subprocess.run( ["docker", "run", "-d", "--rm", "--name", name, "--network", "none", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-c", script], check=True, capture_output=True, text=True, timeout=20, ) eventually(lambda: len(query('raw_requests_total')) == 12) assert value('count(raw_requests_total)') == 12 assert value('count(bounded_requests_total)') == 3 assert value('sum(raw_requests_total)') == value('sum(bounded_requests_total)') == 30 raw_by_route = {x["metric"]["route"]: float(x["value"][1]) for x in query('sum by (route) (raw_requests_total)')} bounded_by_route = {x["metric"]["route"]: float(x["value"][1]) for x in query('bounded_requests_total')} assert raw_by_route == bounded_by_route statistics = get_json('/api/v1/status/tsdb')["data"] counts = {x["name"]: x["value"] for x in statistics["seriesCountByMetricName"]} assert counts["raw_requests_total"] == 12 and counts["bounded_requests_total"] == 3 print(json.dumps({"raw_series": 12, "bounded_series": 3, "snapshot_total": 30, "per_route": raw_by_route, "tsdb_counts_confirmed": True}, indent=2)) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 断言确认原始表示有十二条序列,受控表示有三条,两者总量都为三十,每个路由合计十。执行查询聚合之后,TSDB 仍报告十二条原始序列,证明返回结果变小没有擦除输入。隔离容器没有外部网络、宿主机挂载或公开端口,退出时删除。结果验证身份数量和算术,不是生产容量基准。 ...

一月 10, 2028 · 2 分钟 · XB Field Notes
计数器速率估计主题封面,展示观察、判断与行动三个步骤。

Prometheus rate 估计计数器每秒的增长速度

请求计数器回答进程累计记录了多少事件,速率则回答这些事件到来得有多快。Prometheus 的 rate 估计指定范围内每秒平均增长量,修正能够观测到的计数器重置,并向范围边界进行外推。结果具有明确的时间单位,适合观察负载变化;但它不是两次抓取之间全部事件的精确还原,更不能凭空补回没有采到的重启历史。 采样决定能够推断多少事实 普通计数器在重置前持续增长,进程重启是常见的重置原因。相邻已观测样本出现下降时,计数器函数会按重置处理。不过,如果两次抓取之间已经重启并重新累计,而且新值超过旧样本,重置可能完全不可见。提高抓取频率可以提升观测分辨率,但任何查询都不能确定地恢复一段没有被记录的历史。 应先对每条计数器序列求 rate,然后聚合结果。如果先求和,一台实例的增长可能掩盖另一台实例的重置。标签决定序列身份,替换实例使用新标签时,是开始另一条序列,不是自动接续旧实例。队列长度等仪表值本来就允许正常下降,把它们交给计数器速率函数,会把真实下降错误解释为重置。 选择的范围也是指标含义的一部分 范围选择器左开右闭。在第五分钟求 [5m] 时,恰好位于第零分钟的样本不在范围内。对普通浮点计数器求速率至少需要两个可用样本;只有一个样本时得到空结果,并不表示业务流量为零。因此,范围不仅要覆盖正常抓取周期,还需要考虑偶发漏抓后的样本数量,不能仅凭希望曲线更灵敏就无限缩短。 较长窗口通常会平滑短促突发,也让突发离开平均值的时间更晚;较短窗口响应更快,但观测更少,而且可能变成空结果。应依据事件持续时间与检测目标选择范围,并在面板上明确标注。irate 使用最后两个样本,rate 使用所选范围,两者可以针对同一指标回答不同问题,只要面板清楚说明各自含义,不把数值当成同一种口径比较。 规则评估周期决定何时提出查询问题,不决定目标已经被抓取了多少次。如果目标每分钟才采集一次,即使面板每秒刷新,也不会凭空产生新的观测。分析告警响应速度时,需要分别检查采集与评估这两个周期,不能只缩短页面刷新时间。 坏掉的样子 刚创建的序列可能要等第二个样本到来后才出现在速率面板。把所有空值都补成零,可能掩盖采集故障。遇到空白,应先检查原始序列、时间戳和抓取状态,再判断业务是否空闲。另一种问题是窗口太长:短时间过载已经影响用户,但平均线被长窗口稀释,看起来始终平稳,告警因而没有达到阈值。 重置修正不代表缺口对准确性没有影响。漏抓可能隐藏突发或多次重启,范围边界的外推本身也属于估计。increase 以事件数量单位表达相应的范围估计,即使原始计数器只有整数,也可能返回小数。如果场景需要精确账务或计费总数,应依靠业务记账记录,不能把监控面板描述成可以完整重建交易流水的工具。 快速验证 把程序保存为 rate.py,执行 python3 rate.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,镜像未缓存时可由 Docker 下载。程序调用真实 promtool 查询引擎,输入每分钟一份的合成样本。一次性容器没有外部网络,也不挂载宿主机目录,结束时会自动删除;运行结果由引擎断言决定,不是程序自己模拟查询公式。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "demo_requests_total{instance=\"steady\"}" values: "0+60x5" - series: "demo_requests_total{instance=\"restart\"}" values: "100 160 220 20 80 140" - series: "demo_requests_total{instance=\"gaps\"}" values: "0 60 _ 180 _ 300" - series: "demo_requests_total{instance=\"one\"}" values: "_ _ _ _ _ 7" promql_expr_test: - expr: "rate(demo_requests_total{instance=\"steady\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"steady\"}" value: 1 - expr: "abs(rate(demo_requests_total{instance=\"restart\"}[5m]) - 5/6) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "increase(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 250 - expr: "irate(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"gaps\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"gaps\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"one\"}[5m])" eval_time: 5m exp_samples: [] """ rules = r"""""" script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 断言全部通过时输出 SUCCESS。稳定序列每秒增加一个请求。对于 100 160 220 20 80 140,五分钟 rate 为 5/6,increase 为二百五十,irate 为一。这些结果不同,是因为范围排除了最初样本,随后修正可见重置,再把所得到的平均增长外推到请求时长;二百五十不能被描述为原始样本直接观测到的增量。 ...

十二月 24, 2027 · 2 分钟 · XB Field Notes
告警路由主题封面,展示观察、判断与行动三个步骤。

Alertmanager 通过标签匹配树选择告警接收器

Alertmanager 根据告警标签遍历路由树,选择通知接收器。路由树表达责任归属与升级策略,接收器则定义实际通知集成。配置能够解析,并不代表每一类告警都被分配给正确目的地。因此,不能把重载成功当作路由有效的完成证据;应该先核对进入系统的标签契约,再验证整棵树针对典型告警的选择结果。 既要看匹配,也要看何时停止 每条告警都会进入必须匹配所有告警的根路由。子路由继承没有主动覆盖的配置;没有子节点匹配时,由当前节点处理。否则,遍历通常在第一个匹配的子节点后停止。匹配节点配置 continue: true,才允许继续检查后续同级节点。这不表示配置中的所有接收器都会获得同一条告警,未匹配的节点仍然不会参与。 一个路由节点只能指定一个接收器。接收器内部可以包含多个通知集成,多条匹配路由也可以选择不同接收器,两者是向多个目的地通知的不同方式。不能把路由的 receiver 字段直接换成列表,就期待具有相同行为。选择方案时应核对是否会重复通知,以及不同目的地是否需要独立的分组方式、升级策略和发送节奏。 责任归属依赖稳定的标签契约 匹配 team="payments" 的路由,需要进入 Alertmanager 的告警真正带有这个标签值。名为 team 的注解不能满足该条件。Prometheus 表达式中的聚合可能提前移除一个维度,所以应该检查最终告警标签,包括告警重新标记的影响,不能假定原始指标上的标签会原样保留到通知阶段。原始指标存在团队字段,不足以证明路由可以看到它。 根接收器应当对没有匹配具体子节点的告警仍然有用。这类告警也需要可观察的去向,以及能够修正分类的负责人。测试时除了已知团队,还应覆盖标签缺失:缺少团队的警告可能留在根节点,但缺少同一标签的严重告警仍可能命中独立的严重程度路由。是否缺失与是否完全没有匹配路径,是不同问题。 路由排列顺序本身就是行为。较宽泛的前置路由,可能让后面的具体路由根本没有机会被检查,除非启用了继续匹配。因此,即使匹配器文字没有变化,移动一段配置也可能改变收件方。顺序调整应按策略变化审阅,并把代表性标签输入与配置放在一起,以便持续核对预期选择。 坏掉的样子 拼写错误或标签改名可能使告警落到根接收器,新添加的宽泛子路由可能截走后面团队的告警,而不必要的继续匹配则可能让一个事件产生多份通知。诊断这些问题,需要实际标签和完整匹配路径,单独阅读某个节点通常不够。还应核对新增规则与既有顺序共同作用后的结果,而不只是新增部分自身是否正确。 接收器选择正确仍不等于送达。分组等待、静默、抑制、模板错误、传输故障和目的地行为,都可能在之后介入。先验证路由,再沿所选接收器追踪通知链路。增加规则等待时间不会修复团队匹配条件,缩短分组等待也不能修复接收器凭据缺失。每个阶段都需要对应证据,不能用前一阶段通过代替后一阶段结果。 快速验证 把程序保存为 routing.py,执行 python3 routing.py。需要 Docker 与公开的 Alertmanager 0.28.1 镜像,镜像缺失时可能先下载。真实 amtool 检查配置并验证七组标签输入。实验接收器只有名称,没有实际发送集成,因此可以检查路由选择而不会联系任何人员或外部服务,也不会影响现有告警系统。 import subprocess import uuid # Requires Docker and Alertmanager 0.28.1. No receiver has a delivery integration. script = r"""set -eu cat > /tmp/routes.yml <<'ROUTES' route: receiver: default group_by: [alertname, service] routes: - receiver: payments matchers: ['team="payments"'] continue: true - receiver: pager matchers: ['severity="critical"'] receivers: - name: default - name: payments - name: pager ROUTES /bin/amtool check-config /tmp/routes.yml /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=payments,pager team=payments severity=critical /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=payments team=payments severity=warning /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=pager team=data severity=critical /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=default team=data severity=warning /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=default severity=warning /bin/amtool config routes test --config.file=/tmp/routes.yml --verify.receivers=default team=payments-preview severity=warning sed 's/continue: true/continue: false/' /tmp/routes.yml > /tmp/stop.yml /bin/amtool config routes test --config.file=/tmp/stop.yml --verify.receivers=payments team=payments severity=critical """ name = "xbcatm-route-test-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/alertmanager:v0.28.1", "-s"], input=script, text=True, capture_output=True, timeout=30, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: raise RuntimeError(result.stderr or result.stdout) print("Verified seven route cases, including continuation and unmatched labels.") 第一组结果为 payments,pager:支付团队路由先匹配并继续,严重程度路由随后也匹配。支付团队警告只选择支付接收器,数据团队严重告警选择呼叫接收器;没有匹配或缺少团队值的相应输入进入根接收器。payments-preview 不会匹配精确的支付团队值,避免把名称相近误认为条件成立。 ...

十二月 8, 2027 · 2 分钟 · XB Field Notes
告警状态与通知送达主题封面,展示观察、判断与行动三个步骤。

Prometheus 规则先跟踪告警状态,再交给通知链路

告警规则评估 PromQL 表达式,并持续跟踪表达式返回的标签集。Prometheus 判断每个告警是等待还是触发,Alertmanager 接收告警信息后负责通知路由、分组、静默与抑制。排查时应分别追踪这些环节:规则显示正在触发,是规则状态的证据,不能直接证明某个人已经收到消息。把状态和送达混为一谈,会使漏通知问题很难定位。 表达式返回元素就会激活告警候选项 表达式返回的每个元素代表一个活动告警候选项。queue_depth > 5 这样的比较会过滤不满足条件的数值;但加上 bool 修饰符之后,不满足条件的比较仍会留下数值为零的元素。告警规则依据元素是否存在来激活候选项,因此误用该修饰符,可能让数值已经为零的结果也进入告警流程。这是查询看似合理、规则却持续激活的常见原因。 标签定义告警身份。服务、队列与严重程度等稳定路由维度适合放在标签中,变化中的说明值则适合放在注解中。如果把当前队列大小写进标签,每次数值变化都可能创建一个新身份,重新开始等待。把大小写进注解则可以解释现场状态,同时保留原有告警身份。需要审阅的不只是阈值,还有标签和注解承担的职责。 等待时长通过评估时刻观察 配置 for: 2m 后,一个新出现的标签集会先等待,直到它在评估中持续活动两分钟。如果规则每分钟评估一次,第一分钟首次看到异常,就可能在第三分钟进入触发状态。引擎观察的是评估结果,不是样本之间连续发生的全部物理事实。因此,实际发现时间还包括等待下一次评估的时间,不能只用配置的等待时长计算最坏延迟。 省略 for 时,活动表达式可以在第一次评估就触发。这可能适合含义明确且必须立即处理的故障,但也意味着一个异常样本便足够触发。应根据失败语义主动选择,而不是由于忘记配置而意外采用默认行为。 若表达式在触发前不再返回该标签集,等待状态会清除。本例采用默认恢复行为,已经触发的告警也会在条件消失后清除。显式配置 keep_firing_for 可以在条件消失后继续保持一段触发时间,但本实验没有使用这个选项。不能把所有告警都描述为必须人工确认才能消除的事故记录;规则状态和事故管理系统的关闭流程是不同概念。 坏掉的样子 对缺失序列做阈值比较通常返回空结果。这可能让告警清除,但应用其实已经停止输出指标。应明确缺失是否需要独立规则,不能一律当成恢复。在测试阈值上下变化的同时,也要测试标签变更与样本缺失。语法检查只能证明文件能够解析,并不能揭示这类条件含义错误,更不能证明规则在真实数据中一定符合预期。 告警已经触发却没有页面通知,原因可能是静默、抑制、路由选择、分组等待或发送失败。这些机制属于通知链路,不会改变 Prometheus 对阈值的评估。应先查看活动告警标签,再查看 Alertmanager 匹配了哪个接收器,以及是否真正产生送达证据。延长 for 不会修复接收器配置错误,也不会自动补上路由所需但缺少的标签。 快速验证 把下面程序保存为 alert-state.py,在 Docker 可用时运行 python3 alert-state.py。程序使用 Prometheus 3.5.1,镜像未缓存时可能先下载。真实 promtool 按每分钟一份样本评估队列值 0 6 6 6 0 6 0 0。实验没有启动 Alertmanager,也没有配置任何外部通知目标,所以不会向他人发送测试消息。 ...

九月 1, 2027 · 2 分钟 · XB Field Notes
服务加权比例记录主题封面,展示观察、判断与行动三个步骤。

记录规则应保留比例的分子和分母

当查询成本和复用频率足以支持预计算时,反复使用的服务级查询适合做成记录规则。关键问题是保存什么。如果只保存每个实例的错误百分比,可能丢掉之后聚合必需的流量规模;同时保存错误速率与请求速率,则保留了分子和分母,消费者能够据此计算按实际请求量加权的服务错误占比,而不是把每台实例当成相同流量。 百分比必须对应明确的总体 假设繁忙实例每秒处理十个请求,每秒发生零点一个错误;空闲实例每秒处理一个请求,却每秒发生零点五个错误。两个实例的错误占比分别为百分之一与百分之五十。直接平均得到百分之二十五点五,相当于给两个实例相同权重,但它们的请求量实际上相差十倍。这个数描述平均实例百分比,不描述服务处理的全部请求。 服务合计每秒处理十一个请求,发生零点六个错误,因此错误占比是 0.6/11,约百分之五点四五。这是聚合错误量除以聚合请求量。两侧必须描述同一组符合条件的流量,并使用相同时间窗口。如果分子排除了分母包含的一类失败,或者两侧聚合了不同服务,面板即使计算得很快,结果也仍然不成立。 按顺序建立规则依赖 先对每条计数器序列应用 rate,使已经观测到的重置能够在聚合前得到处理,再按照消费者确实需要的维度求和。本例依次记录服务与实例维度的请求速率、服务请求速率、服务错误速率,以及最终服务错误占比。第一个实例级中间结果用于展示明确的前后依赖,后续规则实际消费这个结果,而不是重复写一个看似相关的名字。 四条规则位于同一个组,按声明顺序运行,使用相同评估时间。分子与分母都保留匹配的 service 标签,因此除法能够把对应服务配对。如果分母额外保留实例标签,就会改变向量匹配关系,可能导致没有结果,或需要重新设计连接方式。测试应同时检查输出标签和数值,不能只看到一个合理百分比就认为查询正确。 命名应说明聚合层级和范围。两分钟速率与三十分钟速率即使当前值恰好相同,也不是同一种量。应在规则旁记录哪些事件计入总体,并明确消费它的面板与告警。沿用旧名称但修改公式,只会改变未来记录样本,不会重新解释已经写入的历史。需要保持时间可比性时,定义变更必须被消费者看见。 坏掉的样子 平均百分比可能让一台低流量故障实例主导整个服务报告,也可能掩盖高流量实例的失败。提高评估频率不会修复这个算术问题,应恢复底层计数或速率,在分别聚合后再相除。如果最后只保留了百分比,通常无法仅靠这些百分比重建原始流量权重,因为相同占比可以来自完全不同的请求数量。 零流量是另一类情况。分母为零时,错误占比并没有被定义为零;分子缺失也可能代表仪表化缺少序列,而不是没有错误。应根据指标契约明确这些语义,避免自动把所有空结果或未定义数值显示成健康的零。还应确认健康期间错误计数器是否持续导出,避免只有发生错误后才创建序列,导致正常状态与数据缺失难以区分。 快速验证 保存为 recording-weighted.py 后执行 python3 recording-weighted.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,首次运行可能下载镜像。真实 promtool 使用每分钟一份的合成计数器样本评估四条记录规则,故意为两个实例提供差异很大的流量权重与错误占比,让错误平均方法无法碰巧得到正确答案。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m rule_files: - /tmp/rules.yml tests: - interval: 1m input_series: - series: "http_requests_total{service=\"checkout\",instance=\"busy\"}" values: "0+600x5" - series: "http_errors_total{service=\"checkout\",instance=\"busy\"}" values: "0+6x5" - series: "http_requests_total{service=\"checkout\",instance=\"quiet\"}" values: "0+60x5" - series: "http_errors_total{service=\"checkout\",instance=\"quiet\"}" values: "0+30x5" promql_expr_test: - expr: "service:http_requests:rate2m" eval_time: 5m exp_samples: - labels: "{__name__=\"service:http_requests:rate2m\",service=\"checkout\"}" value: 11 - expr: "abs(service:http_errors:rate2m - 0.6) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{service=\"checkout\"}" value: 1 - expr: "abs(service:http_errors:ratio_rate2m - 0.6/11) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{service=\"checkout\"}" value: 1 - expr: "abs(avg(rate(http_errors_total[2m]) / rate(http_requests_total[2m])) - 0.255) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{}" value: 1 """ rules = r"""groups: - name: service-rates interval: 1m rules: - record: instance:http_requests:rate2m expr: sum by (service, instance) (rate(http_requests_total[2m])) - record: service:http_requests:rate2m expr: sum by (service) (instance:http_requests:rate2m) - record: service:http_errors:rate2m expr: sum by (service) (rate(http_errors_total[2m])) - record: service:http_errors:ratio_rate2m expr: service:http_errors:rate2m / service:http_requests:rate2m """ script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 预期输出为 SUCCESS。记录请求速率为十一,错误速率为零点六,最终占比为 0.6/11。另外一项断言明确展示,错误的实例比例平均法得到 0.255。涉及浮点除法的比较使用很小的数值容差,标签断言则确认仍保留 checkout 服务身份。这些检查同时覆盖计算次序、依赖输出和最终聚合口径。 ...

八月 31, 2027 · 2 分钟 · XB Field Notes
Prometheus 抓取结果主题封面,展示观察、判断与行动三个步骤。

Prometheus 主动抓取指标,并记录抓取结果

Prometheus 会主动请求每个已配置目标的 HTTP 指标端点。目标负责暴露观测值,采集器决定抓取时刻、解析响应并保存样本。这种安排让采集故障能够从采集器一侧被看见,但不能保证端点一定可达、所有预期目标都被发现,也不能保证用户的登录或下单流程正常。理解这些边界,才能把采集结果转换成准确的告警和排查动作。 一个任务包含多个独立抓取的实例 任务通常把用途相同的目标组织在一起,例如一组接口服务副本。每个目标仍是独立实例,具有自己的抓取结果。常见的 job 与 instance 标签标识这层上下文,最终标签也可能经过重新标记而改变。服务发现提供候选目标,抓取配置再决定实际访问的端点、协议、周期、超时和认证方式。因此,目标没有被发现与目标已经配置但抓取失败,是两类不同的问题。 采集器必须能够连接目标。虽然导出器不需要主动向采集器上传指标,目标仍然需要允许经过授权的采集器访问其入站端口。网络策略、证书身份、认证凭据与路由不会因为采用拉取模式而消失。拉取只是改变连接由哪一方发起,不能把任意导出器变成可以直接暴露在公网的安全接口。部署时要从采集器所在网络检查这条路径。 准确理解系统生成的指标 对于已配置的实例,Prometheus 会生成 up 指标,抓取成功时为一,失败时为零。它是指标名称,不是所谓的 up 标签。抓取耗时与样本数量则补充了采集质量的证据。导出器可能在后端数据库不可用时仍返回合法响应,也可能漏掉一项重要业务指标,因此即使 up 为一,也不能推导后端依赖和全部业务观测都正常。 这些进一步的问题应由应用结果指标、导出器专门提供的依赖状态,以及实际业务探测回答。反过来,up=0 描述的是采集失败,监控网络故障可能使它变为零,而用户请求仍然成功。告警措辞应写清采集目标与错误证据,避免让响应人员把所有监控链路问题直接当成客户故障。业务影响仍需要另一组证据确认。 坏掉的样子 连接被拒绝、请求超时、证书校验失败或导出格式无效,都可能让抓取失败。先查看目标的最近抓取错误,再决定需要调整什么。延长周期无法修复错误端口;增大超时可能让缓慢的导出器看起来恢复,却不会消除它的执行成本。采集变贵时还应检查输出序列数量和响应大小,确认是否新增了不必要的标签组合或昂贵查询。抓取超时不能超过配置周期,缩短周期前则应评估导出器负载。更快的轮询可能成倍增加导出器对数据库的查询次数,却不会降低每次查询的成本。 从服务发现中移除的目标,与继续存在但返回零的目标不同。前者的对应序列之后可能不再出现在即时查询中,所以把全站 up 求和,并不能证明所有预期实例仍然存在。需要完整性保证时,应把发现结果与实际部署清单对照。单独请求一次 /metrics 成功,同样不能代替登录、支付或订单提交的功能验证。 快速验证 把下面程序保存为 pull.py,在 Docker 可用的环境执行 python3 pull.py。程序使用 prom/prometheus:v3.5.1,如果本机尚未缓存镜像,首次运行需要从镜像仓库下载。实验只创建一个一次性容器,让其中的 Prometheus 抓取自己的回环端点,以及回环地址上没有服务监听的端口。它不发布宿主机端口,容器也无法访问外部网络。 import json import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. # All HTTP traffic stays on loopback inside one disposable container. script = r"""set -eu cat > /tmp/prom.yml <<'CONFIG' global: scrape_interval: 1s scrape_timeout: 500ms scrape_configs: - job_name: self static_configs: - targets: ['127.0.0.1:9090'] - job_name: refused static_configs: - targets: ['127.0.0.1:1'] CONFIG /bin/prometheus --config.file=/tmp/prom.yml --storage.tsdb.path=/tmp/data \ --web.listen-address=127.0.0.1:9090 > /tmp/prom.log 2>&1 & for attempt in $(seq 1 30); do if wget -qO /tmp/ready.json 'http://127.0.0.1:9090/api/v1/query?query=count%28up%29%20%3D%3D%202%20and%20sum%28up%29%20%3D%3D%201'; then if grep -q '"status":"success"' /tmp/ready.json && ! grep -q '"result":\[\]' /tmp/ready.json; then wget -qO- 'http://127.0.0.1:9090/api/v1/query?query=up' exit 0 fi fi sleep 1 done cat /tmp/prom.log >&2 exit 1 """ name = "xbcatm-prom-live-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if result.returncode: raise RuntimeError(result.stderr or result.stdout) payload = json.loads(result.stdout) assert payload["status"] == "success" values = {item["metric"]["job"]: float(item["value"][1]) for item in payload["data"]["result"]} assert values == {"self": 1.0, "refused": 0.0}, values print(json.dumps({"up_by_job": values, "scrape_reachability_only": True}, indent=2)) 结果应当包含 self: 1.0 和 refused: 0.0。程序等待两个目标都有抓取结果,再通过查询接口进行断言,因此不会把启动期间数据库暂时为空误判成配置失败。退出清理仅针对本次随机命名的容器,不涉及其他容器。这个结果证明真实采集器可以区分成功抓取与连接拒绝;它没有测试业务功能,也没有证明服务发现清单完整。 ...

八月 30, 2027 · 2 分钟 · XB Field Notes