mirror of
https://github.com/fastapi/fastapi.git
synced 2026-10-09 11:41:39 -04:00
245 lines
9.8 KiB
Python
245 lines
9.8 KiB
Python
"""Environment export remains active alongside independently configured SDKs."""
|
|
|
|
import pytest
|
|
|
|
from ._otlp import otlp_collector
|
|
from ._subprocess import run_in_subprocess
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mode",
|
|
[
|
|
"global",
|
|
"explicit",
|
|
"sentry-first",
|
|
"fastapi-first",
|
|
"sentry-classic",
|
|
"logfire",
|
|
"logfire-opt-out",
|
|
],
|
|
)
|
|
@run_in_subprocess
|
|
def test_environment_export_with_existing_integrations(mode):
|
|
import os
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.telemetry import TelemetryConfig, _runtime
|
|
from fastapi.testclient import TestClient
|
|
from opentelemetry import _logs, metrics, trace
|
|
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
|
|
ExportLogsServiceRequest,
|
|
)
|
|
from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import (
|
|
ExportMetricsServiceRequest,
|
|
)
|
|
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
|
|
ExportTraceServiceRequest,
|
|
)
|
|
from opentelemetry.proto.trace.v1.trace_pb2 import Span
|
|
from opentelemetry.sdk._logs import LoggerProvider
|
|
from opentelemetry.sdk._logs.export import (
|
|
InMemoryLogRecordExporter,
|
|
SimpleLogRecordProcessor,
|
|
)
|
|
from opentelemetry.sdk.metrics import MeterProvider
|
|
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
|
InMemorySpanExporter,
|
|
)
|
|
|
|
with otlp_collector() as (base, received):
|
|
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = base + "/environment"
|
|
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = "x-cloud=preserved"
|
|
os.environ["OTEL_BSP_SCHEDULE_DELAY"] = "60000"
|
|
os.environ["OTEL_BLRP_SCHEDULE_DELAY"] = "60000"
|
|
os.environ["OTEL_METRIC_EXPORT_INTERVAL"] = "60000"
|
|
settings: TelemetryConfig = {}
|
|
original_spans = InMemorySpanExporter()
|
|
original_logs = InMemoryLogRecordExporter()
|
|
original_metrics = InMemoryMetricReader()
|
|
if mode in ("global", "explicit"):
|
|
tp = TracerProvider(shutdown_on_exit=False)
|
|
tp.add_span_processor(SimpleSpanProcessor(original_spans))
|
|
mp = MeterProvider(
|
|
metric_readers=[original_metrics], shutdown_on_exit=False
|
|
)
|
|
lp = LoggerProvider(shutdown_on_exit=False)
|
|
lp.add_log_record_processor(SimpleLogRecordProcessor(original_logs))
|
|
if mode == "global":
|
|
trace.set_tracer_provider(tp)
|
|
metrics.set_meter_provider(mp)
|
|
_logs.set_logger_provider(lp)
|
|
else:
|
|
settings = {
|
|
"tracer_provider": tp,
|
|
"meter_provider": mp,
|
|
"logger_provider": lp,
|
|
}
|
|
elif mode.startswith("logfire"):
|
|
import logfire
|
|
|
|
logfire.configure(
|
|
send_to_logfire=False,
|
|
console=False,
|
|
additional_span_processors=[SimpleSpanProcessor(original_spans)],
|
|
metrics=logfire.MetricsOptions(additional_readers=[original_metrics]),
|
|
advanced=logfire.AdvancedOptions(
|
|
log_record_processors=[SimpleLogRecordProcessor(original_logs)]
|
|
),
|
|
)
|
|
settings["auto_configure"] = mode != "logfire-opt-out"
|
|
else:
|
|
import sentry_sdk
|
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
|
from sentry_sdk.integrations.otlp import OTLPIntegration
|
|
from sentry_sdk.integrations.starlette import StarletteIntegration
|
|
from sentry_sdk.transport import Transport
|
|
|
|
items = []
|
|
|
|
class LocalTransport(Transport):
|
|
def capture_envelope(self, envelope):
|
|
items.extend(item.type for item in envelope.items)
|
|
|
|
if mode == "fastapi-first":
|
|
with TestClient(FastAPI()):
|
|
pass
|
|
integrations = (
|
|
[StarletteIntegration(), FastApiIntegration()]
|
|
if mode == "sentry-classic"
|
|
else [
|
|
OTLPIntegration(
|
|
collector_url=base + "/sentry/traces", setup_propagator=False
|
|
)
|
|
]
|
|
)
|
|
sentry_sdk.init(
|
|
dsn="https://public@example.invalid/1",
|
|
transport=LocalTransport,
|
|
default_integrations=False,
|
|
auto_enabling_integrations=False,
|
|
integrations=integrations,
|
|
traces_sample_rate=1.0,
|
|
send_client_reports=False,
|
|
)
|
|
|
|
app = FastAPI(telemetry=settings)
|
|
|
|
@app.get("/items/{item_id}")
|
|
def endpoint(item_id: int):
|
|
raise ValueError("environment export")
|
|
|
|
try:
|
|
for _ in range(2):
|
|
with TestClient(app, raise_server_exceptions=False) as client:
|
|
assert client.get("/items/1").status_code == 500
|
|
if mode.startswith("logfire"):
|
|
logfire.force_flush()
|
|
elif mode.startswith("sentry") or mode == "fastapi-first":
|
|
tracer_provider = trace.get_tracer_provider()
|
|
assert isinstance(tracer_provider, TracerProvider)
|
|
tracer_provider.force_flush()
|
|
sentry_sdk.flush()
|
|
|
|
def spans_at(path):
|
|
requests = [
|
|
ExportTraceServiceRequest.FromString(body)
|
|
for url, body, _ in received
|
|
if url == path
|
|
]
|
|
return [
|
|
span
|
|
for request in requests
|
|
for resource in request.resource_spans
|
|
for scope in resource.scope_spans
|
|
for span in scope.spans
|
|
]
|
|
|
|
environment_spans = spans_at("/environment/v1/traces")
|
|
multiplier = 2 if mode == "logfire" else 1
|
|
servers = [
|
|
span for span in environment_spans if span.kind == Span.SPAN_KIND_SERVER
|
|
]
|
|
assert len(servers) == 2 * multiplier, (mode, len(servers))
|
|
assert len({span.span_id for span in servers}) == 2
|
|
assert all(span.name == "GET /items/{item_id}" for span in servers)
|
|
if mode in ("global", "explicit"):
|
|
assert sorted(
|
|
(span.context.trace_id, span.context.span_id, span.name)
|
|
for span in original_spans.get_finished_spans()
|
|
) == sorted(
|
|
(
|
|
int.from_bytes(span.trace_id, "big"),
|
|
int.from_bytes(span.span_id, "big"),
|
|
span.name,
|
|
)
|
|
for span in environment_spans
|
|
)
|
|
if mode in ("sentry-first", "fastapi-first"):
|
|
sentry_spans = spans_at("/sentry/traces")
|
|
assert {span.span_id for span in sentry_spans} == {
|
|
span.span_id for span in environment_spans
|
|
}
|
|
if mode == "sentry-classic":
|
|
assert items.count("event") == 2, items
|
|
assert items.count("transaction") == 2, items
|
|
|
|
log_requests = [
|
|
ExportLogsServiceRequest.FromString(body)
|
|
for url, body, _ in received
|
|
if url == "/environment/v1/logs"
|
|
]
|
|
logs = [
|
|
record
|
|
for request in log_requests
|
|
for resource in request.resource_logs
|
|
for scope in resource.scope_logs
|
|
for record in scope.log_records
|
|
]
|
|
assert len(logs) == 2 * multiplier, (mode, len(logs))
|
|
assert all(
|
|
record.trace_id in {span.trace_id for span in servers}
|
|
for record in logs
|
|
)
|
|
metric_requests = [
|
|
ExportMetricsServiceRequest.FromString(body)
|
|
for url, body, _ in received
|
|
if url == "/environment/v1/metrics"
|
|
]
|
|
counts = [
|
|
point.count
|
|
for request in metric_requests
|
|
for resource in request.resource_metrics
|
|
for scope in resource.scope_metrics
|
|
for metric in scope.metrics
|
|
if metric.name == "http.server.request.duration"
|
|
for point in getattr(metric, metric.WhichOneof("data")).data_points
|
|
]
|
|
assert counts and counts[-1] == 2, counts
|
|
assert all(
|
|
headers.get("x-cloud") == "preserved"
|
|
for url, _, headers in received
|
|
if url.startswith("/environment/")
|
|
)
|
|
assert len(_runtime._configured) == (0 if mode == "logfire-opt-out" else 3)
|
|
_runtime._shutdown()
|
|
if mode in ("global", "explicit"):
|
|
# FastAPI's cleanup closes its own exporters, leaving the provider and
|
|
# previously installed vendor components usable.
|
|
count = len(original_spans.get_finished_spans())
|
|
with tp.get_tracer("vendor").start_as_current_span("still-running"):
|
|
pass
|
|
assert len(original_spans.get_finished_spans()) == count + 1
|
|
lp.get_logger("vendor").emit(body="still-running")
|
|
assert len(original_logs.get_finished_logs()) == 3
|
|
assert original_metrics.get_metrics_data() is not None
|
|
tp.shutdown()
|
|
mp.shutdown()
|
|
lp.shutdown()
|
|
finally:
|
|
_runtime._shutdown()
|
|
if mode.startswith("logfire"):
|
|
logfire.shutdown()
|