feat(observability): /health 不再产生 server span

容器 HEALTHCHECK 每 30 秒探一次、上游也在轮询,这些请求各自是一条孤立
trace,量大且没有信息量——和 worker 空转长轮询同一个问题,把观测后台刷满
的正是它们。

instrument_app 传 excluded_urls,由新增的 RAKUTEN_OTEL_EXCLUDED_URLS 控制
(默认 /health$)。按 search 匹配完整 URL,锚点保证不误伤 /api/health-*;
留空时传 None,回落到 OTel 自己的 OTEL_PYTHON_FASTAPI_EXCLUDED_URLS。

三个服务共用 instrument_app,因此抓取/交易/网关一并生效。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 15:24:52 +08:00
co-authored by Claude Opus 5
parent 3c7618a1d6
commit 4cc30bc058
5 changed files with 78 additions and 4 deletions
+56
View File
@@ -15,12 +15,15 @@ from __future__ import annotations
import pytest
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation import fastapi as otel_fastapi
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.trace import StatusCode
from opentelemetry.util.http import parse_excluded_urls
from app.shared import telemetry
from app.shared.config import Settings
@@ -70,6 +73,59 @@ def test_instrument_app_works_before_setup_telemetry(monkeypatch):
FastAPIInstrumentor.uninstrument_app(app)
def _otel_middleware(app: FastAPI) -> OpenTelemetryMiddleware:
"""从构建好的中间件栈里挖出 ASGI 打桩中间件,用于断言它的排除规则"""
node = app.build_middleware_stack()
while node is not None:
if isinstance(node, OpenTelemetryMiddleware):
return node
node = getattr(node, "app", None)
raise AssertionError("中间件栈里没有 OpenTelemetryMiddleware")
def test_health_excluded_from_server_spans(monkeypatch):
"""/health 不产生 server span,其它路径照常
容器 HEALTHCHECK 每 30 秒探一次、上游也在轮询,这些请求各自是一条孤立 trace,
量大且没有信息量。排除规则按 search 匹配完整 URL,所以要钉住两件事:/health
真的被挡掉,且 `$` 锚点没有顺手把 /api/* 一起挡掉。
"""
monkeypatch.setattr(
telemetry, "get_settings", lambda: Settings(_env_file=None, otel_enabled=True)
)
app = FastAPI()
try:
telemetry.instrument_app(app)
excluded = _otel_middleware(app).excluded_urls
assert excluded.url_disabled("http://127.0.0.1:31108/health")
assert not excluded.url_disabled("http://127.0.0.1:31108/api/cart/add")
# 前缀匹配会误伤的反例:锚点保证只有 /health 本身被排除
assert not excluded.url_disabled("http://127.0.0.1:31108/api/health-detail")
finally:
FastAPIInstrumentor.uninstrument_app(app)
def test_excluded_urls_empty_falls_back_to_env(monkeypatch):
"""配置留空时传 None,让 OTel 回落到它自己的环境变量而不是排除空字符串"""
monkeypatch.setattr(
telemetry,
"get_settings",
lambda: Settings(_env_file=None, otel_enabled=True, otel_excluded_urls=" "),
)
# OTel 那份环境变量在模块导入时就解析成常量了,setenv 已经晚了,只能直接替常量
monkeypatch.setattr(
otel_fastapi, "_excluded_urls_from_env", parse_excluded_urls("/metrics")
)
app = FastAPI()
try:
telemetry.instrument_app(app)
excluded = _otel_middleware(app).excluded_urls
assert excluded.url_disabled("http://127.0.0.1:31108/metrics")
assert not excluded.url_disabled("http://127.0.0.1:31108/health")
finally:
FastAPIInstrumentor.uninstrument_app(app)
def test_instrument_app_skipped_when_otel_disabled(monkeypatch):
"""otel 关闭时不装中间件,省掉一层用不上的开销"""
monkeypatch.setattr(