fastapi-observer 1.11.0__tar.gz → 1.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_observer-1.11.0/src/fastapi_observer.egg-info → fastapi_observer-1.12.0}/PKG-INFO +1 -1
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0/src/fastapi_observer.egg-info}/PKG-INFO +1 -1
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/SOURCES.txt +2 -0
- fastapi_observer-1.12.0/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/fastapi.py +3 -0
- fastapi_observer-1.12.0/src/fastapiobserver/otel/exclusions.py +54 -0
- fastapi_observer-1.12.0/src/fastapiobserver/otel/native.py +44 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/tracing.py +10 -13
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/request_context.py +11 -2
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_excluded_url_precedence.py +3 -2
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_middleware.py +14 -3
- fastapi_observer-1.12.0/tests/test_native_telemetry.py +130 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_route_template_cardinality.py +36 -1
- fastapi_observer-1.11.0/src/fastapiobserver/_version.py +0 -1
- fastapi_observer-1.11.0/src/fastapiobserver/otel/exclusions.py +0 -29
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/LICENSE +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/README.md +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/pyproject.toml +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/setup.cfg +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/audit/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/audit/formatter.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/audit/providers.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/audit/verify.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/control_plane.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/db_tracing.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/circuit_breaker.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/filters.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/formatter.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/held.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/queueing.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/setup.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/state.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/volume.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/loop_watchdog.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/metrics/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/metrics/prometheus.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/body_capture.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/context.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/events.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/headers.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/ip.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/metrics.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/request_logging.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/lifecycle.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/logs.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/metrics.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/resource.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/profiles.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/normalize.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/policy.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/proxies.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/redaction.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/builtin.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/discovery.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/factory.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/__init__.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/dlq.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/handler.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/sink.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/protocol.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/registry.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/stats.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_audit_chain.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_config_settings.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_db_tracing.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_dx_parity.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_fork_queue_listener.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_lifespan_teardown.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_log_queue_controls.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_logging_schema_contract.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_loop_watchdog.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_metrics_contracts.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_metrics_optional_dependency.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_negotiate_content_type.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otel_integration.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otel_lifecycle.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otel_log_correlation.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otel_logs_pipeline.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_otlp_export_integration.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_profiles.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_propagation.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_request_insights.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_security_audit_fixes.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_sink_circuit_breaker.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_sinks.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_trust_boundary.py +0 -0
- {fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_utils_refactors.py +0 -0
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/SOURCES.txt
RENAMED
|
@@ -50,6 +50,7 @@ src/fastapiobserver/otel/exclusions.py
|
|
|
50
50
|
src/fastapiobserver/otel/lifecycle.py
|
|
51
51
|
src/fastapiobserver/otel/logs.py
|
|
52
52
|
src/fastapiobserver/otel/metrics.py
|
|
53
|
+
src/fastapiobserver/otel/native.py
|
|
53
54
|
src/fastapiobserver/otel/resource.py
|
|
54
55
|
src/fastapiobserver/otel/settings.py
|
|
55
56
|
src/fastapiobserver/otel/tracing.py
|
|
@@ -87,6 +88,7 @@ tests/test_loop_watchdog.py
|
|
|
87
88
|
tests/test_metrics_contracts.py
|
|
88
89
|
tests/test_metrics_optional_dependency.py
|
|
89
90
|
tests/test_middleware.py
|
|
91
|
+
tests/test_native_telemetry.py
|
|
90
92
|
tests/test_negotiate_content_type.py
|
|
91
93
|
tests/test_otel_integration.py
|
|
92
94
|
tests/test_otel_lifecycle.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.12.0"
|
|
@@ -34,6 +34,7 @@ from .otel import (
|
|
|
34
34
|
install_otel_logs,
|
|
35
35
|
install_otel_metrics,
|
|
36
36
|
)
|
|
37
|
+
from .otel.native import keep_native_logs_out
|
|
37
38
|
from .propagation import install_request_id_propagation
|
|
38
39
|
from .security import SecurityPolicy, TrustedProxyPolicy
|
|
39
40
|
|
|
@@ -217,6 +218,8 @@ def _setup_structured_logging(
|
|
|
217
218
|
otel_settings=otel_settings,
|
|
218
219
|
security_policy=security_policy,
|
|
219
220
|
)
|
|
221
|
+
if otel_log_handler is not None:
|
|
222
|
+
keep_native_logs_out(app)
|
|
220
223
|
if logs_mode == "otlp" and otel_log_handler is None:
|
|
221
224
|
raise RuntimeError(
|
|
222
225
|
"OTLP log mode requires a configured OTLP log handler. "
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, MutableMapping
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from .native import native_telemetry_config
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NativeExclusions:
|
|
13
|
+
"""FastAPI's native ``exclude`` hook, with the URL patterns OpenTelemetry's
|
|
14
|
+
instrumentation uses: *csv*, or its environment variables when *csv* is None.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, csv: str | None) -> None:
|
|
18
|
+
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
|
|
19
|
+
|
|
20
|
+
self.excluded_urls = (
|
|
21
|
+
parse_excluded_urls(csv) if csv is not None else get_excluded_urls("FASTAPI")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def __call__(self, scope: MutableMapping[str, Any]) -> bool:
|
|
25
|
+
return bool(self.excluded_urls.url_disabled(scope.get("path", "")))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def update_otel_middleware_exclusions(
|
|
29
|
+
app: FastAPI, new_exclusions: frozenset[str] | set[str]
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Merge ``new_exclusions`` into the excluded URLs of whichever tracing is active.
|
|
32
|
+
|
|
33
|
+
Both FastAPI's native tracing and ``FastAPIInstrumentor.instrument_app()`` take
|
|
34
|
+
exclusions up front, but routes are usually added after
|
|
35
|
+
``install_observability()``, so this updates them in place.
|
|
36
|
+
"""
|
|
37
|
+
native_exclude = (native_telemetry_config(app) or {}).get("exclude")
|
|
38
|
+
if isinstance(native_exclude, NativeExclusions):
|
|
39
|
+
native_exclude.excluded_urls = _merged(native_exclude.excluded_urls, new_exclusions)
|
|
40
|
+
|
|
41
|
+
current = getattr(app, "middleware_stack", None)
|
|
42
|
+
while current is not None:
|
|
43
|
+
# Match by name so apps without OTel never import the ASGI instrumentation.
|
|
44
|
+
if type(current).__name__ == "OpenTelemetryMiddleware":
|
|
45
|
+
current.excluded_urls = _merged(current.excluded_urls, new_exclusions)
|
|
46
|
+
return
|
|
47
|
+
current = getattr(current, "app", None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _merged(excluded_urls: Any, new_exclusions: Iterable[str]) -> Any:
|
|
51
|
+
from opentelemetry.util.http import parse_excluded_urls
|
|
52
|
+
|
|
53
|
+
existing = getattr(excluded_urls, "_excluded_urls", None) or ()
|
|
54
|
+
return parse_excluded_urls(",".join(sorted({*existing, *new_exclusions})))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""FastAPI's native OpenTelemetry (FastAPI 0.142+), set up to fit fastapi-observer.
|
|
2
|
+
|
|
3
|
+
FastAPI takes these settings only as ``FastAPI(telemetry=...)`` and keeps them in
|
|
4
|
+
``app._telemetry``, which it reads on every request. That attribute isn't public
|
|
5
|
+
API. Without it, as before 0.142, nothing here changes anything, and tracing
|
|
6
|
+
falls back to OpenTelemetry's FastAPI instrumentation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import MutableMapping
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def native_telemetry_config(app: Any) -> MutableMapping[str, Any] | None:
|
|
16
|
+
config = getattr(app, "_telemetry", None)
|
|
17
|
+
return config if isinstance(config, MutableMapping) else None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def trace_natively(app: Any, excluded_urls_csv: str | None) -> bool:
|
|
21
|
+
"""Let FastAPI trace requests itself, skipping the excluded URLs.
|
|
22
|
+
|
|
23
|
+
False when it can't: FastAPI before 0.142, or tracing turned off in
|
|
24
|
+
``FastAPI(telemetry=...)``.
|
|
25
|
+
"""
|
|
26
|
+
config = native_telemetry_config(app)
|
|
27
|
+
if config is None or not config.get("tracing", True):
|
|
28
|
+
return False
|
|
29
|
+
from .exclusions import NativeExclusions
|
|
30
|
+
|
|
31
|
+
config["exclude"] = NativeExclusions(excluded_urls_csv)
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def keep_native_logs_out(app: Any) -> None:
|
|
36
|
+
"""Stop FastAPI exporting its own logs around fastapi-observer's redaction.
|
|
37
|
+
|
|
38
|
+
FastAPI writes validation failures and unhandled exceptions, with messages
|
|
39
|
+
and stack traces, straight to the global logger provider, the one OTLP log
|
|
40
|
+
export sets up. fastapi-observer already logs both, redacted.
|
|
41
|
+
"""
|
|
42
|
+
config = native_telemetry_config(app)
|
|
43
|
+
if config is not None:
|
|
44
|
+
config["logs"] = False
|
|
@@ -11,6 +11,7 @@ from fastapi import FastAPI
|
|
|
11
11
|
from ..config import ObservabilitySettings
|
|
12
12
|
from ..request_context import get_request_state
|
|
13
13
|
from .lifecycle import close_on_app_shutdown
|
|
14
|
+
from .native import trace_natively
|
|
14
15
|
from .resource import (
|
|
15
16
|
build_excluded_urls_csv,
|
|
16
17
|
build_otlp_exporter,
|
|
@@ -39,9 +40,6 @@ def install_otel(
|
|
|
39
40
|
trace_sdk = import_otel_module("opentelemetry.sdk.trace")
|
|
40
41
|
trace_export = import_otel_module("opentelemetry.sdk.trace.export")
|
|
41
42
|
sampling = import_otel_module("opentelemetry.sdk.trace.sampling")
|
|
42
|
-
fastapi_instrumentor_module = import_otel_module(
|
|
43
|
-
"opentelemetry.instrumentation.fastapi"
|
|
44
|
-
)
|
|
45
43
|
|
|
46
44
|
set_trace_sampling_ratio(otel_settings.trace_sampling_ratio)
|
|
47
45
|
tracer_provider = trace_api.get_tracer_provider()
|
|
@@ -100,17 +98,16 @@ def install_otel(
|
|
|
100
98
|
tracer_provider.add_span_processor(_request_call_totals(trace_api, trace_sdk))
|
|
101
99
|
_CALL_TOTALS_PROVIDERS.add(tracer_provider)
|
|
102
100
|
|
|
103
|
-
#
|
|
101
|
+
# FastAPI 0.142+ traces requests itself, with spans for dependencies, the
|
|
102
|
+
# endpoint and serialization; older versions need OpenTelemetry's instrumentation.
|
|
104
103
|
excluded_urls_str = build_excluded_urls_csv(settings)
|
|
105
|
-
|
|
106
|
-
"tracer_provider": tracer_provider
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
app, **instrument_kwargs
|
|
113
|
-
)
|
|
104
|
+
if not trace_natively(app, excluded_urls_str):
|
|
105
|
+
instrument_kwargs: dict[str, Any] = {"tracer_provider": tracer_provider}
|
|
106
|
+
if excluded_urls_str is not None:
|
|
107
|
+
instrument_kwargs["excluded_urls"] = excluded_urls_str
|
|
108
|
+
import_otel_module(
|
|
109
|
+
"opentelemetry.instrumentation.fastapi"
|
|
110
|
+
).FastAPIInstrumentor.instrument_app(app, **instrument_kwargs)
|
|
114
111
|
|
|
115
112
|
if provider_owned:
|
|
116
113
|
close_on_app_shutdown(app, tracer_provider, shutdown=True)
|
|
@@ -69,8 +69,17 @@ class RequestState:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def route_template(scope: Mapping[str, Any]) -> str | None:
|
|
72
|
-
"""The matched route's template, such as ``/users/{user_id}``, once routing has run.
|
|
73
|
-
|
|
72
|
+
"""The matched route's template, such as ``/users/{user_id}``, once routing has run.
|
|
73
|
+
|
|
74
|
+
Since FastAPI 0.137, ``scope["route"].path`` leaves out prefixes given to
|
|
75
|
+
``include_router()`` and those of outer routers. FastAPI keeps the full
|
|
76
|
+
template in its effective route context, set for routes in included routers.
|
|
77
|
+
"""
|
|
78
|
+
fastapi_scope = scope.get("fastapi")
|
|
79
|
+
context = (
|
|
80
|
+
fastapi_scope.get("effective_route_context") if isinstance(fastapi_scope, dict) else None
|
|
81
|
+
)
|
|
82
|
+
template = getattr(context, "path", None) or getattr(scope.get("route"), "path", None)
|
|
74
83
|
return template if isinstance(template, str) and template else None
|
|
75
84
|
|
|
76
85
|
|
|
@@ -72,7 +72,8 @@ def test_install_otel_passes_empty_excluded_urls(
|
|
|
72
72
|
monkeypatch: pytest.MonkeyPatch,
|
|
73
73
|
) -> None:
|
|
74
74
|
"""Integration: install_otel actually passes excluded_urls='' to instrument_app
|
|
75
|
-
when otel_excluded_urls is explicitly set to empty.
|
|
75
|
+
when otel_excluded_urls is explicitly set to empty. FastAPI's own tracing is
|
|
76
|
+
off, so the instrumentation is used, as on FastAPI before 0.142."""
|
|
76
77
|
pytest.importorskip("opentelemetry.instrumentation.fastapi")
|
|
77
78
|
|
|
78
79
|
from fastapi import FastAPI
|
|
@@ -87,7 +88,7 @@ def test_install_otel_passes_empty_excluded_urls(
|
|
|
87
88
|
|
|
88
89
|
monkeypatch.setattr(FastAPIInstrumentor, "instrument_app", spy_instrument_app)
|
|
89
90
|
|
|
90
|
-
app = FastAPI()
|
|
91
|
+
app = FastAPI(telemetry={"tracing": False})
|
|
91
92
|
settings = ObservabilitySettings(otel_excluded_urls="") # type: ignore[arg-type]
|
|
92
93
|
otel_settings = OTelSettings(enabled=True, service_name="test-svc")
|
|
93
94
|
|
|
@@ -122,8 +122,18 @@ def test_middleware_classifies_unhandled_exception(caplog) -> None:
|
|
|
122
122
|
assert request_logs[-1].event["http.response.status_code"] == 500
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
("telemetry", "events", "error_type"),
|
|
127
|
+
[
|
|
128
|
+
({}, [], "RuntimeError"), # FastAPI's own tracing: no exception text in the trace
|
|
129
|
+
({"tracing": False}, ["exception"], None), # the instrumentation records it once
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
def test_unhandled_exception_marks_the_server_span_once(
|
|
126
133
|
monkeypatch: pytest.MonkeyPatch,
|
|
134
|
+
telemetry: dict[str, bool],
|
|
135
|
+
events: list[str],
|
|
136
|
+
error_type: str | None,
|
|
127
137
|
) -> None:
|
|
128
138
|
in_memory = pytest.importorskip("opentelemetry.sdk.trace.export.in_memory_span_exporter")
|
|
129
139
|
import fastapiobserver.otel.tracing as otel_tracing_module
|
|
@@ -132,7 +142,7 @@ def test_unhandled_exception_recorded_once_on_server_span(
|
|
|
132
142
|
exporter = in_memory.InMemorySpanExporter()
|
|
133
143
|
monkeypatch.setattr(otel_tracing_module, "build_otlp_exporter", lambda *_: exporter)
|
|
134
144
|
|
|
135
|
-
app = FastAPI()
|
|
145
|
+
app = FastAPI(telemetry=telemetry) # type: ignore[arg-type]
|
|
136
146
|
|
|
137
147
|
@app.get("/boom-span")
|
|
138
148
|
def boom() -> None:
|
|
@@ -153,8 +163,9 @@ def test_unhandled_exception_recorded_once_on_server_span(
|
|
|
153
163
|
[server_span] = [
|
|
154
164
|
span for span in exporter.get_finished_spans() if span.kind.name == "SERVER"
|
|
155
165
|
]
|
|
156
|
-
assert [event.name for event in server_span.events] ==
|
|
166
|
+
assert [event.name for event in server_span.events] == events
|
|
157
167
|
assert server_span.status.status_code.name == "ERROR"
|
|
168
|
+
assert server_span.attributes.get("error.type") == error_type
|
|
158
169
|
|
|
159
170
|
|
|
160
171
|
def test_extract_scope_client_ip_contract() -> None:
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""FastAPI 0.142+ traces requests itself; fastapi-observer uses that and keeps
|
|
2
|
+
FastAPI's own log export out of the way of redaction."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from fastapi.testclient import TestClient
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from fastapiobserver import (
|
|
15
|
+
ObservabilitySettings,
|
|
16
|
+
OTelLogsSettings,
|
|
17
|
+
OTelSettings,
|
|
18
|
+
install_observability,
|
|
19
|
+
)
|
|
20
|
+
from fastapiobserver.logging import setup_logging, shutdown_logging
|
|
21
|
+
import fastapiobserver.otel.logs as otel_logs_module
|
|
22
|
+
import fastapiobserver.otel.tracing as otel_tracing_module
|
|
23
|
+
|
|
24
|
+
pytest.importorskip("fastapi.telemetry")
|
|
25
|
+
trace_api = pytest.importorskip("opentelemetry.trace")
|
|
26
|
+
in_memory_spans = pytest.importorskip("opentelemetry.sdk.trace.export.in_memory_span_exporter")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Collector(logging.Handler):
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.payloads: list[dict[str, Any]] = []
|
|
33
|
+
|
|
34
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
35
|
+
self.payloads.append(json.loads(self.format(record)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _server_spans(exporter: Any) -> list[Any]:
|
|
39
|
+
trace_api.get_tracer_provider().force_flush()
|
|
40
|
+
return [s for s in exporter.get_finished_spans() if s.kind == trace_api.SpanKind.SERVER]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_requests_are_traced_by_fastapi_itself(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
44
|
+
exporter = in_memory_spans.InMemorySpanExporter()
|
|
45
|
+
monkeypatch.setattr(otel_tracing_module, "build_otlp_exporter", lambda *_: exporter)
|
|
46
|
+
settings = ObservabilitySettings()
|
|
47
|
+
app = FastAPI()
|
|
48
|
+
|
|
49
|
+
@app.get("/orders/{order_id}")
|
|
50
|
+
def order(order_id: str) -> dict[str, str]:
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
@app.get("/healthz")
|
|
54
|
+
def healthz() -> dict[str, str]:
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
@app.get("/internal", include_in_schema=False)
|
|
58
|
+
def internal() -> dict[str, str]:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
install_observability(app, settings, otel_settings=OTelSettings(enabled=True))
|
|
62
|
+
collector = _Collector()
|
|
63
|
+
setup_logging(settings, logs_mode="otlp", extra_handlers=[collector])
|
|
64
|
+
with TestClient(app) as client: # the lifespan adds hidden routes to the exclusions
|
|
65
|
+
for path in ("/orders/1", "/healthz", "/internal"):
|
|
66
|
+
client.get(path)
|
|
67
|
+
shutdown_logging()
|
|
68
|
+
|
|
69
|
+
[span] = _server_spans(exporter) # excluded and hidden routes aren't traced
|
|
70
|
+
assert span.instrumentation_scope.name == "fastapi"
|
|
71
|
+
assert span.name == "GET /orders/{order_id}"
|
|
72
|
+
[completed] = [
|
|
73
|
+
p
|
|
74
|
+
for p in collector.payloads
|
|
75
|
+
if p["message"] == "request.completed" and p["event"]["path"] == "/orders/1"
|
|
76
|
+
]
|
|
77
|
+
assert completed["trace_id"] == f"{span.context.trace_id:032x}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_instrumentation_traces_when_fastapi_tracing_is_off(
|
|
81
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
82
|
+
) -> None:
|
|
83
|
+
exporter = in_memory_spans.InMemorySpanExporter()
|
|
84
|
+
monkeypatch.setattr(otel_tracing_module, "build_otlp_exporter", lambda *_: exporter)
|
|
85
|
+
app = FastAPI(telemetry={"tracing": False}) # as on FastAPI before 0.142
|
|
86
|
+
|
|
87
|
+
@app.get("/orders/{order_id}")
|
|
88
|
+
def order(order_id: str) -> dict[str, str]:
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
install_observability(app, ObservabilitySettings(), otel_settings=OTelSettings(enabled=True))
|
|
92
|
+
TestClient(app).get("/orders/1")
|
|
93
|
+
shutdown_logging()
|
|
94
|
+
|
|
95
|
+
[span] = _server_spans(exporter)
|
|
96
|
+
assert span.instrumentation_scope.name == "opentelemetry.instrumentation.fastapi"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize("tracing", [False, True])
|
|
100
|
+
def test_fastapi_does_not_export_its_own_logs_past_redaction(
|
|
101
|
+
monkeypatch: pytest.MonkeyPatch, tracing: bool
|
|
102
|
+
) -> None:
|
|
103
|
+
in_memory_logs = pytest.importorskip("opentelemetry.sdk._logs.export")
|
|
104
|
+
logs_api = pytest.importorskip("opentelemetry._logs")
|
|
105
|
+
exported = in_memory_logs.InMemoryLogRecordExporter()
|
|
106
|
+
monkeypatch.setattr(otel_logs_module, "build_otlp_exporter", lambda *_: exported)
|
|
107
|
+
monkeypatch.setattr(
|
|
108
|
+
otel_tracing_module,
|
|
109
|
+
"build_otlp_exporter",
|
|
110
|
+
lambda *_: in_memory_spans.InMemorySpanExporter(),
|
|
111
|
+
)
|
|
112
|
+
app = FastAPI()
|
|
113
|
+
|
|
114
|
+
@app.get("/boom")
|
|
115
|
+
def boom() -> dict[str, str]:
|
|
116
|
+
raise RuntimeError("upstream rejected token=sk_live_secret")
|
|
117
|
+
|
|
118
|
+
install_observability(
|
|
119
|
+
app,
|
|
120
|
+
ObservabilitySettings(),
|
|
121
|
+
otel_settings=OTelSettings(enabled=tracing),
|
|
122
|
+
otel_logs_settings=OTelLogsSettings(enabled=True, logs_mode="otlp"),
|
|
123
|
+
)
|
|
124
|
+
TestClient(app, raise_server_exceptions=False).get("/boom")
|
|
125
|
+
shutdown_logging()
|
|
126
|
+
logs_api.get_logger_provider().force_flush()
|
|
127
|
+
|
|
128
|
+
scopes = {record.instrumentation_scope.name for record in exported.get_finished_logs()}
|
|
129
|
+
assert "fastapiobserver.middleware" in scopes # ours, redacted, still exported
|
|
130
|
+
assert "fastapi" not in scopes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_route_template_cardinality.py
RENAMED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
|
-
from fastapi import FastAPI
|
|
7
|
+
from fastapi import APIRouter, FastAPI
|
|
8
8
|
from fastapi.testclient import TestClient
|
|
9
9
|
|
|
10
10
|
from fastapiobserver import ObservabilitySettings, install_observability
|
|
@@ -88,3 +88,38 @@ def test_scanner_404s_share_one_metrics_label() -> None:
|
|
|
88
88
|
assert after_probes - before == 20
|
|
89
89
|
assert unmatched_requests() == after_probes
|
|
90
90
|
assert not any(path in client.get("/metrics").text for path in probes)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_routers_included_with_a_prefix_keep_it_in_the_label() -> None:
|
|
94
|
+
# Since FastAPI 0.137, scope["route"].path leaves out prefixes given to
|
|
95
|
+
# include_router() and those of outer routers (fastapi/fastapi#16051).
|
|
96
|
+
app = FastAPI()
|
|
97
|
+
v1, v2, inner = APIRouter(), APIRouter(), APIRouter()
|
|
98
|
+
|
|
99
|
+
@v1.get("/items/{item_id}")
|
|
100
|
+
def v1_item(item_id: str) -> dict[str, str]:
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
@v2.get("/items/{item_id}")
|
|
104
|
+
def v2_item(item_id: str) -> dict[str, str]:
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
@inner.get("/ping")
|
|
108
|
+
def ping() -> dict[str, str]:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
v2.include_router(inner, prefix="/admin")
|
|
112
|
+
app.include_router(v1, prefix="/v1")
|
|
113
|
+
app.include_router(v2, prefix="/v2")
|
|
114
|
+
install_observability(app, ObservabilitySettings(metrics_enabled=True))
|
|
115
|
+
client = TestClient(app)
|
|
116
|
+
for path in ("/v1/items/1", "/v2/items/1", "/v2/admin/ping"):
|
|
117
|
+
assert client.get(path).status_code == 200
|
|
118
|
+
|
|
119
|
+
labels = {
|
|
120
|
+
line.split('path="', 1)[1].split('"', 1)[0]
|
|
121
|
+
for line in client.get("/metrics").text.splitlines()
|
|
122
|
+
if line.startswith("http_requests_total{")
|
|
123
|
+
}
|
|
124
|
+
assert {"/v1/items/{item_id}", "/v2/items/{item_id}", "/v2/admin/ping"} <= labels
|
|
125
|
+
assert "/items/{item_id}" not in labels
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.11.0"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
if TYPE_CHECKING:
|
|
6
|
-
from fastapi import FastAPI
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def update_otel_middleware_exclusions(
|
|
10
|
-
app: FastAPI, new_exclusions: frozenset[str] | set[str]
|
|
11
|
-
) -> None:
|
|
12
|
-
"""Merge ``new_exclusions`` into the built OTel middleware's excluded URLs.
|
|
13
|
-
|
|
14
|
-
``FastAPIInstrumentor.instrument_app(excluded_urls=...)`` only takes
|
|
15
|
-
exclusions up front, but routes are usually added after
|
|
16
|
-
``install_observability()`` and OTel offers no supported way to extend
|
|
17
|
-
them later, so this updates the middleware in place.
|
|
18
|
-
"""
|
|
19
|
-
current = getattr(app, "middleware_stack", None)
|
|
20
|
-
while current is not None:
|
|
21
|
-
# Match by name so apps without OTel never import the ASGI instrumentation.
|
|
22
|
-
if type(current).__name__ == "OpenTelemetryMiddleware":
|
|
23
|
-
from opentelemetry.util.http import parse_excluded_urls
|
|
24
|
-
|
|
25
|
-
existing = getattr(current.excluded_urls, "_excluded_urls", None) or ()
|
|
26
|
-
merged = sorted({*existing, *new_exclusions})
|
|
27
|
-
current.excluded_urls = parse_excluded_urls(",".join(merged))
|
|
28
|
-
return
|
|
29
|
-
current = getattr(current, "app", None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapi_observer.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/integrations/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/integrations/strawberry.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/circuit_breaker.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/logging/formatter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/metrics/prometheus.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/body_capture.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/context.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/events.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/headers.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/middleware/metrics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/normalize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/security/redaction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/dlq.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/handler.py
RENAMED
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/src/fastapiobserver/sinks/logtail/sink.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.11.0 → fastapi_observer-1.12.0}/tests/test_metrics_optional_dependency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|