服务发现产生候选目标及其元数据,目标重新标记在 Prometheus 抓取之前处理这些候选项。它可以保留或过滤目标、改变访问地址,并创建随采集序列一起保存的标签。这与改写单个抓取样本发生在不同阶段。先弄清操作阶段,才能避免本来只想去掉一个指标,却因为配置位置错误而把整个目标从采集范围中移除。
定义规则前先查看发现输入
发现结果包含 __address__ 等内部标签,以及由具体发现机制提供的 __meta_ 字段。文件发现中的 __meta_filepath 指明目标来自哪个文件。这些元数据在目标重新标记期间可以使用,但内部标签会在该阶段结束后从最终目标标签集中移除。如果后续查询需要某个值,应明确把它复制到普通标签中,不能假定所有发现信息都会自动入库。
准确的元数据名称取决于发现机制。从 Kubernetes 发现配置复制过来的规则,不会自动适用于文件发现。组合输入时,缺少的源标签贡献空值,因此依赖一个不存在字段的保留规则,可能过滤所有候选项。排查网络故障之前,应通过目标接口或发现视图检查实际输入标签,先确认目标是否真的进入了抓取阶段。
规则顺序决定最终身份
重新标记步骤按照配置顺序执行。keep 动作仅保留组合源标签值匹配正则表达式的目标,drop 动作则移除匹配目标。正则表达式默认在两端锚定。replace 动作可以把捕获的文本复制到目标标签,而新值又能成为同一序列中后续规则的输入。因此,互换两个步骤可能改变结果,即使每条规则本身都能够通过解析。
复制一个值并不会自动删除原标签。确实需要删除时,它是另一项操作,而且不能破坏抓取或消费者依赖的身份信息。修改 __address__ 会改变 Prometheus 实际访问的端点,修改普通标签则改变上下文。两者都不能简单描述成给面板字段换一个好看的名字:地址变化可能影响可达性,标签变化则可能影响路由、聚合和历史查询。
目标标签能够影响从该目标收集的所有序列。应选择服务或有意义的部署范围等稳定字段,而不是复制全部发现注解。频繁变化的构建标识,可能让大量指标同时产生新身份。因此,发现配置中的一个小改动,能够影响远大于配置文件自身规模的存储数据及其消费者,需要根据真实查询需求审阅。
坏掉的样子
被重新标记过滤的目标,不是一个 up=0 的已配置目标。它没有被抓取,失败抓取查询可能永远不会报告它。预期服务消失时,应对照发现候选项、被过滤目标与活动目标。若告警假定全部预期成员始终存在对应 up 序列,一条错误保留规则就可能让整组目标看起来像安静且健康的空集合。
复制错误元数据也可能得到外观合理、语义却错误的归属标签。查询结果很整齐,不代表数据真的属于被标注的服务。应一起审阅执行顺序、捕获组和最终标签。大范围删除标签还可能合并原本不同的身份,目标重新标记不会为了补偿信息损失而自动计算聚合值,不能把去标签当成汇总方法。
如果没有规则设置 instance,Prometheus 会使用最终的 __address__ 作为默认值,因此改写地址可能同时改变访问端点与默认身份。还应检查 honor_labels:目标导出标签与抓取侧标签冲突时,结果由这个配置决定,不能笼统认为目标标签一定覆盖导出器提交的所有同名值。
快速验证
把程序保存为 target-relabel.py,执行 python3 target-relabel.py。需要 Docker 和 Prometheus 3.5.1,镜像未缓存时可能下载。隔离容器内同时提供静态文本指标端点与文件发现清单,清单包含一个接口目标和一个应被过滤的目标。指标路径使用 /metrics.txt,使 HTTP 响应具有 Prometheus 接受的文本内容类型,避免端点格式问题遮住标签实验。
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: discovered
metrics_path: /metrics.txt
file_sd_configs:
- files: ['/tmp/targets/*.json']
relabel_configs:
- source_labels: [role]
regex: api
action: keep
- source_labels: [__meta_filepath]
regex: '.*/([^/]+)[.]json'
target_label: discovery_file
replacement: '$1'
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
cat > /tmp/targets/lab.json <<'TARGETS'
[{"targets": ["127.0.0.1:8080"], "labels": {"role": "api"}}, {"targets": ["127.0.0.1:1"], "labels": {"role": "discard"}}]
TARGETS
/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: value('up{job="discovered"}') == 1)
targets = get_json('/api/v1/targets?state=any')["data"]
assert len(targets["activeTargets"]) == 1 and len(targets["droppedTargets"]) == 1
active = targets["activeTargets"][0]
dropped = targets["droppedTargets"][0]
assert active["discoveredLabels"]["__meta_filepath"] == "/tmp/targets/lab.json"
assert dropped["discoveredLabels"]["role"] == "discard"
labels = query('up{job="discovered"}')[0]["metric"]
assert labels["discovery_file"] == "lab" and labels["role"] == "api"
assert not any(label.startswith("__meta_") for label in labels)
print(json.dumps({"active": 1, "dropped": 1, "stored_labels": labels,
"source_file": active["discoveredLabels"]["__meta_filepath"]}, indent=2))
finally:
subprocess.run(["docker", "rm", "-f", name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
程序等待真实采集发生,再检查一个活动目标与一个被过滤目标。活动目标的发现元数据包含 /tmp/targets/lab.json,保存的 up 序列具有 role="api" 和 discovery_file="lab",没有遗留的 __meta_ 标签。另一个目标可以在过滤证据中看到,但没有被当成一次失败抓取。这些结果区分了候选输入、选择结果与最终存储身份。
全部请求留在容器回环地址,没有外部网络、宿主机挂载或公开端口,退出时删除唯一名称的容器。断言验证真实文件发现、过滤、元数据复制和最终标签,没有验证其他发现提供方的字段,也不能证明生产清单已经覆盖全部预期服务。
来源
- Prometheus: configuration,有序目标重新标记与文件发现元数据,已核验 2026-09-13。
- Prometheus: jobs and instances,目标身份与抓取生成指标,已核验 2026-09-13。
- Prometheus: HTTP API,检查活动目标与被过滤目标,已核验 2026-09-13。