Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 41 additions & 26 deletions dnstapir/opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -89,51 +91,64 @@ 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(
settings: OtlpSettings,
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})
if _configured:
logger.debug("OpenTelemetry already configured, ignoring settings")
return

# Configure tracing provider
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(settings.get_spans_exporter())
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())
# 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
# 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.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",
],
)
logger.warning("OpenTelemetry not configured, no exporters available")
28 changes: 11 additions & 17 deletions tests/test_opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Loading