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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user