From 791d77296d05bed40f98b9d76144a6c4f93ef81b Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 19 Oct 2026 17:33:59 +0200 Subject: [PATCH 1/2] Do not log metrics/spans to console by default --- dnstapir/opentelemetry.py | 78 +++++++++++++++++++++---------------- tests/test_opentelemetry.py | 28 ++++++------- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/dnstapir/opentelemetry.py b/dnstapir/opentelemetry.py index fa693a2..c1eb5e1 100644 --- a/dnstapir/opentelemetry.py +++ b/dnstapir/opentelemetry.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -OtlpUrl = Annotated[Url, UrlConstraints(allowed_schemes=["http", "https", "grpc"])] +OtlpUrl = Annotated[Url, UrlConstraints(allowed_schemes=["http", "https", "grpc", "console"])] DEFAULT_OTLP_SERVICE_NAME = "dnstapir" DEFAULT_OTLP_GRPC_PORT = 4317 @@ -35,11 +35,11 @@ class OtlpSettings(BaseModel): service_name: str | None = None spans_endpoint: OtlpUrl | None = Field( default=None, - description="The endpoint for OTLP spans, e.g., 'grpc://localhost:4317' or 'http://localhost:4318/v1/traces'", + description="Endpoint for OTLP spans, e.g., 'grpc://localhost:4317' or 'http://localhost:4318/v1/traces'", ) metrics_endpoint: OtlpUrl | None = Field( default=None, - description="The endpoint for OTLP metrics, e.g., 'grpc://localhost:4317' or 'http://localhost:4318/v1/metrics'", + description="Endpoint for OTLP metrics, e.g., 'grpc://localhost:4317' or 'http://localhost:4318/v1/metrics'", ) insecure_grpc: bool | None = Field( default=None, @@ -65,7 +65,7 @@ def check_grpc_path(cls, value: OtlpUrl | None) -> OtlpUrl | None: raise ValueError("gRPC query parameter not supported") return value - def get_spans_exporter(self) -> SpanExporter: + def get_spans_exporter(self) -> SpanExporter | None: """Get the appropriate OTLP span exporter based on the spans endpoint scheme.""" if self.spans_endpoint: match self.spans_endpoint.scheme: @@ -76,9 +76,11 @@ def get_spans_exporter(self) -> SpanExporter: ) case "http" | "https": return OTLPSpanExporterHttp(endpoint=str(self.spans_endpoint)) - return ConsoleSpanExporter() + case "console": + return ConsoleSpanExporter() + return None - def get_metrics_exporter(self) -> MetricExporter: + def get_metrics_exporter(self) -> MetricExporter | None: """Get the appropriate OTLP metric exporter based on the metrics endpoint scheme.""" if self.metrics_endpoint: match self.metrics_endpoint.scheme: @@ -89,7 +91,9 @@ def get_metrics_exporter(self) -> MetricExporter: ) case "http" | "https": return OTLPMetricExporterHttp(endpoint=str(self.metrics_endpoint)) - return ConsoleMetricExporter() + case "console": + return ConsoleMetricExporter() + return None def configure_opentelemetry( @@ -105,35 +109,43 @@ def configure_opentelemetry( resource = Resource(attributes={SERVICE_NAME: service_name}) # Configure tracing provider - trace_provider = TracerProvider(resource=resource) - processor = BatchSpanProcessor(settings.get_spans_exporter()) - trace_provider.add_span_processor(processor) - trace.set_tracer_provider(trace_provider) - logger.debug("OTLP spans via %s", settings.spans_endpoint or "console") + if spans_exporter := settings.get_spans_exporter(): + trace_provider = TracerProvider(resource=resource) + processor = BatchSpanProcessor(spans_exporter) + trace_provider.add_span_processor(processor) + trace.set_tracer_provider(trace_provider) + logger.debug("OTLP spans via %s", settings.spans_endpoint or "console") + else: + logger.debug("No OTLP spans exporter configured, tracing disabled") # Configure metrics provider - reader = PeriodicExportingMetricReader(settings.get_metrics_exporter()) - meter_provider = MeterProvider(resource=resource, metric_readers=[reader]) - metrics.set_meter_provider(meter_provider) - logger.debug("OTLP metrics via %s", settings.metrics_endpoint or "console") + if metrics_exporter := settings.get_metrics_exporter(): + reader = PeriodicExportingMetricReader(metrics_exporter) + meter_provider = MeterProvider(resource=resource, metric_readers=[reader]) + metrics.set_meter_provider(meter_provider) + logger.debug("OTLP metrics via %s", settings.metrics_endpoint or "console") + else: + logger.debug("No OTLP metrics exporter configured, metrics disabled") # Configure instrumentation for various libraries - PymongoInstrumentor().instrument() - BotocoreInstrumentor().instrument() - RedisInstrumentor().instrument() - HTTPX2ClientInstrumentor().instrument() - - _configured = True - logger.info("OpenTelemetry configured") + if spans_exporter or metrics_exporter: + PymongoInstrumentor().instrument() + BotocoreInstrumentor().instrument() + RedisInstrumentor().instrument() + HTTPX2ClientInstrumentor().instrument() + + if fastapi_app: + FastAPIInstrumentor.instrument_app( + app=fastapi_app, + http_capture_headers_server_request=[ + "x-request-id", + "traceparent", + "tracestate", + ], + ) + _configured = True + logger.info("OpenTelemetry configured") + else: + logger.warning("OpenTelemetry not configured, no exporters available") else: logger.debug("OpenTelemetry already configured, ignoring settings") - - if fastapi_app: - FastAPIInstrumentor.instrument_app( - app=fastapi_app, - http_capture_headers_server_request=[ - "x-request-id", - "traceparent", - "tracestate", - ], - ) diff --git a/tests/test_opentelemetry.py b/tests/test_opentelemetry.py index 8f2f83e..aeb54a9 100644 --- a/tests/test_opentelemetry.py +++ b/tests/test_opentelemetry.py @@ -26,6 +26,14 @@ def test_telemetry_settings(): res = OtlpSettings() assert res.spans_endpoint is None assert res.metrics_endpoint is None + + res = OtlpSettings(metrics_endpoint="console://", spans_endpoint="console://") + assert res.spans_endpoint.scheme == "console" + assert res.spans_endpoint.host is None + assert res.spans_endpoint.port is None + assert res.metrics_endpoint.scheme == "console" + assert res.metrics_endpoint.host is None + assert res.metrics_endpoint.port is None assert isinstance(res.get_spans_exporter(), ConsoleSpanExporter) assert isinstance(res.get_metrics_exporter(), ConsoleMetricExporter) @@ -121,24 +129,10 @@ def test_exporters_otlp( metrics_exporter.shutdown() -def test_telemetry_console(): - """Test configuring OpenTelemetry with a FastAPI app.""" - - app = FastAPI() - settings = OtlpSettings() - configure_opentelemetry(service_name="test", settings=settings, fastapi_app=app) - - -def test_telemetry_twice(): - """Test that configuring OpenTelemetry twice does not reconfigure it.""" - - settings = OtlpSettings() - configure_opentelemetry(service_name="test", settings=settings) - tracer_provider = trace.get_tracer_provider() - meter_provider = metrics.get_meter_provider() +def test_telemetry_fastapi(): + """Test OpenTelemetry with a FastAPI app.""" + settings = OtlpSettings(spans_endpoint="console://", metrics_endpoint="console://") app = FastAPI() configure_opentelemetry(service_name="test", settings=settings, fastapi_app=app) - assert trace.get_tracer_provider() is tracer_provider - assert metrics.get_meter_provider() is meter_provider assert app._is_instrumented_by_opentelemetry From e11e2c913f70344473f838fa14da98d8adee048f Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 19 Oct 2026 18:20:19 +0200 Subject: [PATCH 2/2] Clarify usage --- dnstapir/opentelemetry.py | 91 ++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/dnstapir/opentelemetry.py b/dnstapir/opentelemetry.py index c1eb5e1..857d318 100644 --- a/dnstapir/opentelemetry.py +++ b/dnstapir/opentelemetry.py @@ -101,51 +101,54 @@ def configure_opentelemetry( service_name: str | None = None, fastapi_app: FastAPI | None = None, ) -> None: + """Configure OpenTelemetry for tracing and metrics. Should only be called only once per process.""" + global _configured # Providers can only be set once per process, so only configure once - if not _configured: - service_name = settings.service_name or service_name or DEFAULT_OTLP_SERVICE_NAME - resource = Resource(attributes={SERVICE_NAME: service_name}) - - # Configure tracing provider - if spans_exporter := settings.get_spans_exporter(): - trace_provider = TracerProvider(resource=resource) - processor = BatchSpanProcessor(spans_exporter) - trace_provider.add_span_processor(processor) - trace.set_tracer_provider(trace_provider) - logger.debug("OTLP spans via %s", settings.spans_endpoint or "console") - else: - logger.debug("No OTLP spans exporter configured, tracing disabled") - - # Configure metrics provider - if metrics_exporter := settings.get_metrics_exporter(): - reader = PeriodicExportingMetricReader(metrics_exporter) - meter_provider = MeterProvider(resource=resource, metric_readers=[reader]) - metrics.set_meter_provider(meter_provider) - logger.debug("OTLP metrics via %s", settings.metrics_endpoint or "console") - else: - logger.debug("No OTLP metrics exporter configured, metrics disabled") - - # Configure instrumentation for various libraries - if spans_exporter or metrics_exporter: - PymongoInstrumentor().instrument() - BotocoreInstrumentor().instrument() - RedisInstrumentor().instrument() - HTTPX2ClientInstrumentor().instrument() - - if fastapi_app: - FastAPIInstrumentor.instrument_app( - app=fastapi_app, - http_capture_headers_server_request=[ - "x-request-id", - "traceparent", - "tracestate", - ], - ) - _configured = True - logger.info("OpenTelemetry configured") - else: - logger.warning("OpenTelemetry not configured, no exporters available") - else: + if _configured: logger.debug("OpenTelemetry already configured, ignoring settings") + return + + service_name = settings.service_name or service_name or DEFAULT_OTLP_SERVICE_NAME + resource = Resource(attributes={SERVICE_NAME: service_name}) + + # Configure tracing provider + if spans_exporter := settings.get_spans_exporter(): + trace_provider = TracerProvider(resource=resource) + processor = BatchSpanProcessor(spans_exporter) + trace_provider.add_span_processor(processor) + trace.set_tracer_provider(trace_provider) + logger.debug("OTLP spans via %s", settings.spans_endpoint or "console") + else: + logger.debug("No OTLP spans exporter configured, tracing disabled") + + # Configure metrics provider + if metrics_exporter := settings.get_metrics_exporter(): + reader = PeriodicExportingMetricReader(metrics_exporter) + meter_provider = MeterProvider(resource=resource, metric_readers=[reader]) + metrics.set_meter_provider(meter_provider) + logger.debug("OTLP metrics via %s", settings.metrics_endpoint or "console") + else: + logger.debug("No OTLP metrics exporter configured, metrics disabled") + + # Configure instrumentation for various libraries + if spans_exporter or metrics_exporter: + PymongoInstrumentor().instrument() + BotocoreInstrumentor().instrument() + RedisInstrumentor().instrument() + HTTPX2ClientInstrumentor().instrument() + + if fastapi_app: + FastAPIInstrumentor.instrument_app( + app=fastapi_app, + http_capture_headers_server_request=[ + "x-request-id", + "traceparent", + "tracestate", + ], + ) + _configured = True + logger.info("OpenTelemetry configured") + else: + logger.warning("OpenTelemetry not configured, no exporters available")