fastapi-observer 1.8.0__tar.gz → 1.9.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.8.0/src/fastapi_observer.egg-info → fastapi_observer-1.9.0}/PKG-INFO +2 -2
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/README.md +1 -1
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0/src/fastapi_observer.egg-info}/PKG-INFO +2 -2
- fastapi_observer-1.9.0/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/events.py +21 -16
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/metrics.py +5 -1
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/request_logging.py +1 -1
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_middleware.py +3 -2
- fastapi_observer-1.9.0/tests/test_route_template_cardinality.py +90 -0
- fastapi_observer-1.8.0/src/fastapiobserver/_version.py +0 -1
- fastapi_observer-1.8.0/tests/test_route_template_cardinality.py +0 -51
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/LICENSE +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/pyproject.toml +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/setup.cfg +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapi_observer.egg-info/SOURCES.txt +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/audit/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/audit/formatter.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/audit/providers.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/audit/verify.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/control_plane.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/db_tracing.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/fastapi.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/circuit_breaker.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/filters.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/formatter.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/queueing.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/setup.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/state.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/metrics/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/metrics/prometheus.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/body_capture.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/context.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/headers.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/ip.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/exclusions.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/lifecycle.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/logs.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/metrics.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/resource.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/otel/tracing.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/profiles.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/request_context.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/security/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/security/normalize.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/security/policy.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/security/proxies.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/security/redaction.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/builtin.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/discovery.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/factory.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/logtail/__init__.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/logtail/dlq.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/logtail/handler.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/logtail/sink.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/protocol.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/registry.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/stats.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_audit_chain.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_config_settings.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_db_tracing.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_dx_parity.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_excluded_url_precedence.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_fork_queue_listener.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_lifespan_teardown.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_log_queue_controls.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_logging_schema_contract.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_metrics_contracts.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_metrics_optional_dependency.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_negotiate_content_type.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otel_integration.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otel_lifecycle.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otel_log_correlation.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otel_logs_pipeline.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_otlp_export_integration.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_profiles.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_propagation.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_security_audit_fixes.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_sink_circuit_breaker.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_sinks.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_trust_boundary.py +0 -0
- {fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/tests/test_utils_refactors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-observer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Zero-glue FastAPI observability with security presets and runtime controls
|
|
5
5
|
Author-email: Vitaee <opensource@vitaee.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -149,7 +149,7 @@ Explicit variables and Python arguments always win over profile defaults. Every
|
|
|
149
149
|
- **One JSON line per request** with method, path, status, duration and client IP, and your service's name, environment and version on every line, including your own `logging` calls.
|
|
150
150
|
- **Request IDs** taken from `X-Request-ID` when the caller is a trusted proxy, generated otherwise, returned in the response and passed on to the services you call, so one ID finds a request's logs in every service.
|
|
151
151
|
- **Redaction on by default.** Passwords, tokens, API keys, `Authorization` and cookies are masked before anything is written. Presets cover stricter needs (`strict`, `pci`, `gdpr`).
|
|
152
|
-
- **Prometheus metrics** labelled by route template (`/orders/{order_id}`), so
|
|
152
|
+
- **Prometheus metrics** labelled by route template (`/orders/{order_id}`), and 404s for paths no route matches share one `<unmatched>` label, so neither IDs nor scanners explode cardinality.
|
|
153
153
|
- **OpenTelemetry traces** with `trace_id` and `span_id` in every log line, and optional log export over OTLP.
|
|
154
154
|
- **Non-blocking log writes.** Lines are written from a background thread, so a slow stdout never stalls the event loop.
|
|
155
155
|
|
|
@@ -73,7 +73,7 @@ Explicit variables and Python arguments always win over profile defaults. Every
|
|
|
73
73
|
- **One JSON line per request** with method, path, status, duration and client IP, and your service's name, environment and version on every line, including your own `logging` calls.
|
|
74
74
|
- **Request IDs** taken from `X-Request-ID` when the caller is a trusted proxy, generated otherwise, returned in the response and passed on to the services you call, so one ID finds a request's logs in every service.
|
|
75
75
|
- **Redaction on by default.** Passwords, tokens, API keys, `Authorization` and cookies are masked before anything is written. Presets cover stricter needs (`strict`, `pci`, `gdpr`).
|
|
76
|
-
- **Prometheus metrics** labelled by route template (`/orders/{order_id}`), so
|
|
76
|
+
- **Prometheus metrics** labelled by route template (`/orders/{order_id}`), and 404s for paths no route matches share one `<unmatched>` label, so neither IDs nor scanners explode cardinality.
|
|
77
77
|
- **OpenTelemetry traces** with `trace_id` and `span_id` in every log line, and optional log export over OTLP.
|
|
78
78
|
- **Non-blocking log writes.** Lines are written from a background thread, so a slow stdout never stalls the event loop.
|
|
79
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-observer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Zero-glue FastAPI observability with security presets and runtime controls
|
|
5
5
|
Author-email: Vitaee <opensource@vitaee.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -149,7 +149,7 @@ Explicit variables and Python arguments always win over profile defaults. Every
|
|
|
149
149
|
- **One JSON line per request** with method, path, status, duration and client IP, and your service's name, environment and version on every line, including your own `logging` calls.
|
|
150
150
|
- **Request IDs** taken from `X-Request-ID` when the caller is a trusted proxy, generated otherwise, returned in the response and passed on to the services you call, so one ID finds a request's logs in every service.
|
|
151
151
|
- **Redaction on by default.** Passwords, tokens, API keys, `Authorization` and cookies are masked before anything is written. Presets cover stricter needs (`strict`, `pci`, `gdpr`).
|
|
152
|
-
- **Prometheus metrics** labelled by route template (`/orders/{order_id}`), so
|
|
152
|
+
- **Prometheus metrics** labelled by route template (`/orders/{order_id}`), and 404s for paths no route matches share one `<unmatched>` label, so neither IDs nor scanners explode cardinality.
|
|
153
153
|
- **OpenTelemetry traces** with `trace_id` and `span_id` in every log line, and optional log export over OTLP.
|
|
154
154
|
- **Non-blocking log writes.** Lines are written from a background thread, so a slow stdout never stalls the event loop.
|
|
155
155
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.9.0"
|
|
@@ -68,23 +68,28 @@ def _classify_error(status_code: int, error: Exception | None) -> str:
|
|
|
68
68
|
return "ok"
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
"""Return the matched route template for bounded metric cardinality.
|
|
71
|
+
UNMATCHED_ROUTE = "<unmatched>"
|
|
73
72
|
|
|
74
|
-
After Starlette's router runs, ``scope["route"]`` contains the matched
|
|
75
|
-
``Route`` object whose ``.path`` is the template string
|
|
76
|
-
(e.g. ``/users/{user_id}``).
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
def _extract_route_template(scope: Scope, raw_path: str, status_code: int) -> str:
|
|
75
|
+
"""Return the metrics ``path`` label: the matched route template if known.
|
|
76
|
+
|
|
77
|
+
After routing, ``scope["route"]`` holds the matched route, whose ``.path``
|
|
78
|
+
is the template (e.g. ``/users/{user_id}``). Without one, a 404 means no
|
|
79
|
+
route matched, mostly scanners, so those share ``UNMATCHED_ROUTE`` instead
|
|
80
|
+
of adding a time series per URL. Other requests without a template keep
|
|
81
|
+
*raw_path*: FastAPI doesn't record plain Starlette routes, such as ones
|
|
82
|
+
added with ``app.add_route()``, in the scope.
|
|
81
83
|
"""
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
template
|
|
85
|
-
|
|
86
|
-
return template
|
|
87
|
-
return raw_path
|
|
84
|
+
template = getattr(scope.get("route"), "path", None)
|
|
85
|
+
if isinstance(template, str) and template:
|
|
86
|
+
return template
|
|
87
|
+
return UNMATCHED_ROUTE if status_code == 404 else raw_path
|
|
88
88
|
|
|
89
|
-
__all__ = [
|
|
90
|
-
|
|
89
|
+
__all__ = [
|
|
90
|
+
"UNMATCHED_ROUTE",
|
|
91
|
+
"_RequestEventBuilder",
|
|
92
|
+
"_classify_error",
|
|
93
|
+
"_extract_route_template",
|
|
94
|
+
"_sanitize_exception_message",
|
|
95
|
+
]
|
|
@@ -38,12 +38,16 @@ class _MetricsRecorder:
|
|
|
38
38
|
if scope and "app" in scope:
|
|
39
39
|
app = scope["app"]
|
|
40
40
|
state = getattr(app.state, "_observability_state", None)
|
|
41
|
-
|
|
41
|
+
# Filled when the app starts; without a lifespan, keep the settings' list.
|
|
42
|
+
if state is not None and state.excluded_urls:
|
|
42
43
|
excluded_urls = state.excluded_urls
|
|
43
44
|
|
|
45
|
+
# Also check the request's own path: unmatched requests share one label.
|
|
46
|
+
raw_path = scope.get("path") if scope else None
|
|
44
47
|
if (
|
|
45
48
|
path in excluded_urls
|
|
46
49
|
or collapsed_path in excluded_urls
|
|
50
|
+
or raw_path in excluded_urls
|
|
47
51
|
):
|
|
48
52
|
return
|
|
49
53
|
try:
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/request_logging.py
RENAMED
|
@@ -168,7 +168,7 @@ class RequestLoggingMiddleware:
|
|
|
168
168
|
)
|
|
169
169
|
|
|
170
170
|
# Prefer route template for bounded cardinality
|
|
171
|
-
route_template = _extract_route_template(scope, path)
|
|
171
|
+
route_template = _extract_route_template(scope, path, status_code)
|
|
172
172
|
|
|
173
173
|
self.metrics_recorder.observe(
|
|
174
174
|
method=method,
|
|
@@ -183,10 +183,11 @@ def test_resolve_request_id_contract() -> None:
|
|
|
183
183
|
def test_extract_route_template_contract() -> None:
|
|
184
184
|
from fastapiobserver.middleware import _extract_route_template
|
|
185
185
|
|
|
186
|
-
assert _extract_route_template({}, "/raw") == "/raw"
|
|
186
|
+
assert _extract_route_template({}, "/raw", 200) == "/raw"
|
|
187
|
+
assert _extract_route_template({}, "/raw", 404) == "<unmatched>"
|
|
187
188
|
|
|
188
189
|
scope = {
|
|
189
190
|
"endpoint": lambda: None,
|
|
190
191
|
"route": type("Route", (), {"path": "/users/{user_id}"})(),
|
|
191
192
|
}
|
|
192
|
-
assert _extract_route_template(scope, "/raw") == "/users/{user_id}"
|
|
193
|
+
assert _extract_route_template(scope, "/raw", 404) == "/users/{user_id}"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Tests for route template extraction to prevent metric cardinality explosion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
|
|
10
|
+
from fastapiobserver import ObservabilitySettings, install_observability
|
|
11
|
+
from fastapiobserver.middleware import _extract_route_template # type: ignore[attr-defined]
|
|
12
|
+
from fastapiobserver.middleware.events import UNMATCHED_ROUTE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeRoute:
|
|
16
|
+
"""Minimal route stub matching Starlette's Route interface."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path: str) -> None:
|
|
19
|
+
self.path = path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_route_template_preferred_over_raw_path() -> None:
|
|
23
|
+
"""When scope has a matched route, use its template."""
|
|
24
|
+
scope = {"route": _FakeRoute("/users/{user_id}")}
|
|
25
|
+
result = _extract_route_template(scope, "/users/alice", 200)
|
|
26
|
+
assert result == "/users/{user_id}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_unmatched_404_gets_the_shared_label() -> None:
|
|
30
|
+
"""No route matched: a 404 must not put its raw path in the label."""
|
|
31
|
+
scope: dict = {} # no "route" key
|
|
32
|
+
assert _extract_route_template(scope, "/nonexistent/path", 404) == UNMATCHED_ROUTE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_plain_route_without_template_keeps_raw_path() -> None:
|
|
36
|
+
"""FastAPI records no route for plain Starlette routes, such as `/metrics`."""
|
|
37
|
+
scope: dict = {}
|
|
38
|
+
assert _extract_route_template(scope, "/metrics", 200) == "/metrics"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_fallback_when_route_has_no_path_attr() -> None:
|
|
42
|
+
"""Mounted sub-apps may have routes without .path attribute."""
|
|
43
|
+
|
|
44
|
+
class _MountedRoute:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
scope = {"route": _MountedRoute()}
|
|
48
|
+
assert _extract_route_template(scope, "/mounted/raw", 200) == "/mounted/raw"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_route_is_none() -> None:
|
|
52
|
+
"""scope["route"] explicitly set to None."""
|
|
53
|
+
scope = {"route": None}
|
|
54
|
+
assert _extract_route_template(scope, "/somewhere", 404) == UNMATCHED_ROUTE
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_nested_route_template() -> None:
|
|
58
|
+
"""Deeply nested route with multiple parameters."""
|
|
59
|
+
scope = {"route": _FakeRoute("/orgs/{org_id}/teams/{team_id}/members")}
|
|
60
|
+
result = _extract_route_template(scope, "/orgs/acme-corp/teams/eng/members", 200)
|
|
61
|
+
assert result == "/orgs/{org_id}/teams/{team_id}/members"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_scanner_404s_share_one_metrics_label() -> None:
|
|
65
|
+
app = FastAPI()
|
|
66
|
+
|
|
67
|
+
@app.get("/orders/{order_id}")
|
|
68
|
+
def order(order_id: int) -> dict[str, int]:
|
|
69
|
+
return {"order_id": order_id}
|
|
70
|
+
|
|
71
|
+
install_observability(app, ObservabilitySettings(metrics_enabled=True))
|
|
72
|
+
client = TestClient(app) # no lifespan: exclusions must still apply
|
|
73
|
+
|
|
74
|
+
def unmatched_requests() -> float:
|
|
75
|
+
return sum(
|
|
76
|
+
float(line.rsplit(" ", 1)[1])
|
|
77
|
+
for line in client.get("/metrics").text.splitlines()
|
|
78
|
+
if line.startswith("http_requests_total{") and f'path="{UNMATCHED_ROUTE}"' in line
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
before = unmatched_requests()
|
|
82
|
+
probes = [f"/probe-{uuid.uuid4().hex[:8]}.php" for _ in range(20)]
|
|
83
|
+
for path in probes:
|
|
84
|
+
assert client.get(path).status_code == 404
|
|
85
|
+
after_probes = unmatched_requests()
|
|
86
|
+
client.get("/healthz") # excluded by default, though no route serves it
|
|
87
|
+
|
|
88
|
+
assert after_probes - before == 20
|
|
89
|
+
assert unmatched_requests() == after_probes
|
|
90
|
+
assert not any(path in client.get("/metrics").text for path in probes)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.8.0"
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"""Tests for route template extraction to prevent metric cardinality explosion."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from fastapiobserver.middleware import _extract_route_template # type: ignore[attr-defined]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class _FakeRoute:
|
|
9
|
-
"""Minimal route stub matching Starlette's Route interface."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, path: str) -> None:
|
|
12
|
-
self.path = path
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_route_template_preferred_over_raw_path() -> None:
|
|
16
|
-
"""When scope has a matched route, use its template."""
|
|
17
|
-
scope = {"route": _FakeRoute("/users/{user_id}")}
|
|
18
|
-
result = _extract_route_template(scope, "/users/alice")
|
|
19
|
-
assert result == "/users/{user_id}"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_fallback_to_raw_path_when_no_route() -> None:
|
|
23
|
-
"""404 / unmatched — no route in scope → raw path used."""
|
|
24
|
-
scope: dict = {} # no "route" key
|
|
25
|
-
result = _extract_route_template(scope, "/nonexistent/path")
|
|
26
|
-
assert result == "/nonexistent/path"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_fallback_when_route_has_no_path_attr() -> None:
|
|
30
|
-
"""Mounted sub-apps may have routes without .path attribute."""
|
|
31
|
-
|
|
32
|
-
class _MountedRoute:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
scope = {"route": _MountedRoute()}
|
|
36
|
-
result = _extract_route_template(scope, "/mounted/raw")
|
|
37
|
-
assert result == "/mounted/raw"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_route_is_none() -> None:
|
|
41
|
-
"""scope["route"] explicitly set to None."""
|
|
42
|
-
scope = {"route": None}
|
|
43
|
-
result = _extract_route_template(scope, "/somewhere")
|
|
44
|
-
assert result == "/somewhere"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_nested_route_template() -> None:
|
|
48
|
-
"""Deeply nested route with multiple parameters."""
|
|
49
|
-
scope = {"route": _FakeRoute("/orgs/{org_id}/teams/{team_id}/members")}
|
|
50
|
-
result = _extract_route_template(scope, "/orgs/acme-corp/teams/eng/members")
|
|
51
|
-
assert result == "/orgs/{org_id}/teams/{team_id}/members"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapi_observer.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapi_observer.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.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
|
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/integrations/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/integrations/strawberry.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/logging/circuit_breaker.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.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/middleware/body_capture.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/logtail/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.8.0 → fastapi_observer-1.9.0}/src/fastapiobserver/sinks/logtail/handler.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
|
|
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
|