Files
Раис Юсупалиев 66beab4682 015 add tracing
2026-03-14 00:28:51 +03:00

186 lines
6.2 KiB
Python

import importlib
from fastapi import FastAPI
from app.config import ObservabilityConfig
def _reload_tracing_module():
tracing_module = importlib.import_module("app.runtime.tracing")
return importlib.reload(tracing_module)
def _make_observability_config(**overrides) -> ObservabilityConfig:
payload = {
"enabled": True,
"service_name": "delivery-aggregator",
"otlp_endpoint": "http://collector:4317",
"otlp_insecure": True,
}
payload.update(overrides)
return ObservabilityConfig(**payload)
def test_configure_tracing_skips_bootstrap_when_disabled(monkeypatch) -> None:
tracing_module = _reload_tracing_module()
calls = {"provider": 0, "fastapi": 0, "httpx": 0, "redis": 0}
def fake_get_or_create_tracer_provider(_observability):
calls["provider"] += 1
return object()
def fake_instrument_fastapi_app(_app, *, tracer_provider) -> None:
del tracer_provider
calls["fastapi"] += 1
def fake_instrument_httpx(*, tracer_provider) -> None:
del tracer_provider
calls["httpx"] += 1
def fake_instrument_redis(*, tracer_provider) -> None:
del tracer_provider
calls["redis"] += 1
monkeypatch.setattr(
tracing_module,
"_get_or_create_tracer_provider",
fake_get_or_create_tracer_provider,
)
monkeypatch.setattr(
tracing_module,
"_instrument_fastapi_app",
fake_instrument_fastapi_app,
)
monkeypatch.setattr(tracing_module, "_instrument_httpx", fake_instrument_httpx)
monkeypatch.setattr(tracing_module, "_instrument_redis", fake_instrument_redis)
tracing_module.configure_tracing(
FastAPI(),
_make_observability_config(enabled=False),
)
assert calls == {"provider": 0, "fastapi": 0, "httpx": 0, "redis": 0}
def test_build_tracer_provider_uses_otlp_exporter_and_service_name(monkeypatch) -> None:
tracing_module = _reload_tracing_module()
created: dict[str, object] = {}
class FakeExporter:
def __init__(self, *, endpoint: str, insecure: bool) -> None:
created["endpoint"] = endpoint
created["insecure"] = insecure
class FakeBatchSpanProcessor:
def __init__(self, exporter: object) -> None:
self.exporter = exporter
created["processor_exporter"] = exporter
class FakeTracerProvider:
def __init__(self, *, resource) -> None:
self.resource = resource
self.span_processors: list[object] = []
def add_span_processor(self, processor: object) -> None:
self.span_processors.append(processor)
monkeypatch.setattr(tracing_module, "OTLPSpanExporter", FakeExporter)
monkeypatch.setattr(tracing_module, "BatchSpanProcessor", FakeBatchSpanProcessor)
monkeypatch.setattr(tracing_module, "TracerProvider", FakeTracerProvider)
provider = tracing_module._build_tracer_provider(
_make_observability_config(
service_name="g2s-aggregator",
otlp_endpoint="http://trace-backend:4317",
otlp_insecure=False,
)
)
assert created["endpoint"] == "http://trace-backend:4317"
assert created["insecure"] is False
assert provider.resource.attributes["service.name"] == "g2s-aggregator"
assert len(provider.span_processors) == 1
assert created["processor_exporter"] is provider.span_processors[0].exporter
def test_configure_tracing_reuses_provider_and_global_instrumentation(monkeypatch) -> None:
tracing_module = _reload_tracing_module()
created = {
"exporters": 0,
"providers": 0,
"set_provider_calls": 0,
"fastapi_apps": [],
"httpx_calls": 0,
"redis_calls": 0,
}
class FakeExporter:
def __init__(self, *, endpoint: str, insecure: bool) -> None:
created["exporters"] += 1
self.endpoint = endpoint
self.insecure = insecure
class FakeBatchSpanProcessor:
def __init__(self, exporter: object) -> None:
self.exporter = exporter
class FakeTracerProvider:
def __init__(self, *, resource) -> None:
created["providers"] += 1
self.resource = resource
self.span_processors: list[object] = []
def add_span_processor(self, processor: object) -> None:
self.span_processors.append(processor)
class FakeInstrumentor:
def __init__(self, kind: str) -> None:
self.kind = kind
self.is_instrumented_by_opentelemetry = False
def instrument(self, **kwargs) -> None:
assert kwargs["tracer_provider"] is not None
created[f"{self.kind}_calls"] += 1
self.is_instrumented_by_opentelemetry = True
class FakeFastAPIInstrumentor:
@staticmethod
def instrument_app(app: FastAPI, *, tracer_provider) -> None:
assert tracer_provider is not None
created["fastapi_apps"].append(app)
app._is_instrumented_by_opentelemetry = True
def fake_set_tracer_provider(_provider: object) -> None:
created["set_provider_calls"] += 1
monkeypatch.setattr(tracing_module, "OTLPSpanExporter", FakeExporter)
monkeypatch.setattr(tracing_module, "BatchSpanProcessor", FakeBatchSpanProcessor)
monkeypatch.setattr(tracing_module, "TracerProvider", FakeTracerProvider)
monkeypatch.setattr(
tracing_module,
"_HTTPX_INSTRUMENTOR",
FakeInstrumentor("httpx"),
)
monkeypatch.setattr(
tracing_module,
"_REDIS_INSTRUMENTOR",
FakeInstrumentor("redis"),
)
monkeypatch.setattr(tracing_module, "FastAPIInstrumentor", FakeFastAPIInstrumentor)
monkeypatch.setattr(tracing_module.trace, "set_tracer_provider", fake_set_tracer_provider)
app_one = FastAPI()
app_two = FastAPI()
observability = _make_observability_config()
tracing_module.configure_tracing(app_one, observability)
tracing_module.configure_tracing(app_one, observability)
tracing_module.configure_tracing(app_two, observability)
assert created["exporters"] == 1
assert created["providers"] == 1
assert created["set_provider_calls"] == 1
assert created["fastapi_apps"] == [app_one, app_two]
assert created["httpx_calls"] == 1
assert created["redis_calls"] == 1