Prometheus 存在多个重新标记阶段,相似语法容易遮住不同作用。目标重新标记在采集之前处理发现目标,样本重新标记则在抓取之后、入库之前处理采集样本。前一个阶段的 drop 可以让某个端点不被抓取,后一个阶段的相同动作则从已经获取的响应中丢弃匹配样本。首先确定阶段,才能判断配置到底改变了哪一部分观测。
先决定删除数据,还是删除标签
在 metric_relabel_configs 中,action: drop 匹配组合后的源标签值,并丢弃匹配样本。使用 __name__ 作为输入时,可以拒绝指定指标族。它不是删除这个源标签却保留样本的意思。如果对一个广泛存在的值使用过宽条件,可能丢掉远超预期的数据,所以不能仅凭动作名称中有“删除”就认为它只影响元数据。
action: labeldrop 则把正则表达式用于标签名称,从保留样本中移除匹配标签;labelkeep 只保留名称匹配的标签。两者都不会求和或生成汇总。前一类操作删除观测,后一类操作改变身份,应该按照预期数据契约选择,而不是把名称相近的动作当成可互换参数。留下数值但删掉维度,也可能比直接过滤一部分样本带来更隐蔽的问题。
重新标记表达式默认在两端锚定。替换或过滤规则需要在对应阶段使用正确的源标签与模式。发现元数据通常属于目标阶段,到样本处理阶段时,已不能继续期待使用 __meta_ 输入。需要保留的上下文应提前复制到普通目标标签,而不是假定每个导出指标都会携带内部发现字段。配置语法相同,不代表输入集合也相同。
删除维度之后仍须保持唯一身份
时间序列由指标名和完整标签集合识别。移除区分两条观测的标签,可能让它们身份重叠,产生重复样本冲突或丢失区别。Prometheus 不会自动推断这些数值该求和、取平均还是任选其一。如果真正需要服务总量,应优先采用受控维度的仪表化以及明确查询聚合,而不是把标签删除当成自动汇总机制。
本例只在剩余路由身份本来就唯一的情况下删除构建标签,这是受控前提,不表示可以从任意导出器移除任何昂贵标签。上线前应使用有代表性的输出检查碰撞,也应核对引用被删标签的面板和告警。抓取成功只能说明采集过程完成,不能保证这些消费者仍会选择正确数据,更不能保证历史与新数据可以直接比较。
坏掉的样子
把样本过滤规则放进目标重新标记中,可能移除整个端点,而不是一类昂贵指标。反过来,样本过滤也不会阻止导出器生成响应,或阻止响应字节经过网络。它减少的是入库保留的样本。如果昂贵部分在采集本身,还需要处理仪表化或导出器行为,不能把存储侧过滤描述为同时消除了全部上游成本。
up 等自动生成的抓取序列不经过样本重新标记,因此在这个阶段尝试删除 up,不会隐藏系统生成的指标。健康的 up 可以与被主动丢弃的业务指标同时存在,解释数据缺失时应结合过滤配置。修改规则影响未来入库,不会重写已经保存的历史标签,所以查询跨越变更时刻时也可能看到两种身份。
应把 scrape_samples_post_metric_relabeling 与目标错误、up 一起观察。配置的样本数量上限在样本重新标记之后生效,超出时会让抓取失败,并不是任意保留前面一部分额度内样本。因此,主动过滤与入库上限失败需要不同诊断,即便二者都表现为预期业务序列缺失。
快速验证
保存为 metric-relabel.py 后执行 python3 metric-relabel.py。需要 Docker 与 Prometheus 3.5.1,必要时由 Docker 获取镜像。同一个静态文本端点分别被原始任务与过滤任务抓取。路径使用 .txt 以提供文本内容类型,启动过程等待真正出现样本,不假定服务刚启动时目标列表已经填充完成。
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: raw
metrics_path: /metrics.txt
static_configs:
- targets: ['127.0.0.1:8080']
- job_name: filtered
metrics_path: /metrics.txt
static_configs:
- targets: ['127.0.0.1:8080']
metric_relabel_configs:
- source_labels: [__name__]
regex: 'debug_.+|up'
action: drop
- regex: build_sha
action: labeldrop
PROM_CONFIG
cat > /tmp/www/metrics.txt <<'METRIC_DATA'
# TYPE demo_requests_total counter
demo_requests_total{route="/orders",build_sha="release-a"} 42
# TYPE debug_alloc_bytes gauge
debug_alloc_bytes{kind="scratch"} 9
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('demo_requests_total')) == 2)
raw = query('demo_requests_total{job="raw"}')[0]
filtered = query('demo_requests_total{job="filtered"}')[0]
assert raw["metric"]["build_sha"] == "release-a"
assert "build_sha" not in filtered["metric"]
assert float(raw["value"][1]) == float(filtered["value"][1]) == 42
assert len(query('debug_alloc_bytes{job="raw"}')) == 1
assert query('debug_alloc_bytes{job="filtered"}') == []
assert value('up{job="filtered"}') == 1
print(json.dumps({"raw_labels": raw["metric"], "filtered_labels": filtered["metric"],
"debug_series_dropped": True, "generated_up_preserved": True}, indent=2))
finally:
subprocess.run(["docker", "rm", "-f", name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
原始请求序列保留 build_sha="release-a",过滤后的序列移除该标签,但保留路由与数值四十二。调试指标存在于原始任务,在过滤任务中消失。虽然 drop 模式也写了 up,过滤任务自动生成的 up 仍为一。断言通过真实查询接口逐项核对这些差异,分别证明样本被过滤、标签被删除以及自动生成序列的例外。
两个任务具有不同的任务标签,本实验在删除标签后也没有竞争的请求序列。因此,它验证阶段语义,不证明任意迁移都不会发生身份碰撞。所有流量留在容器回环地址,不挂载宿主机目录,也不发布端口,结束时仅清理实验容器。将相同策略用于生产前,仍应验证真实导出器身份与受影响消费者。
来源
Prometheus:任务与实例,抓取结果与保留样本计数,已核验 2026-10-15。
Prometheus: configuration,样本重新标记、样本过滤、标签删除与自动生成序列的例外,已核验 2026-09-13。
Prometheus: data model,指标名与完整标签集合定义序列身份,已核验 2026-09-13。
Prometheus: HTTP API,查询保留的指标标签与数值,已核验 2026-09-13。