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