fastapi-observer 1.4.0__tar.gz → 1.5.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.
Files changed (122) hide show
  1. {fastapi_observer-1.4.0/src/fastapi_observer.egg-info → fastapi_observer-1.5.0}/PKG-INFO +22 -22
  2. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/README.md +18 -16
  3. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/pyproject.toml +4 -6
  4. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0/src/fastapi_observer.egg-info}/PKG-INFO +22 -22
  5. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapi_observer.egg-info/requires.txt +1 -6
  6. fastapi_observer-1.5.0/src/fastapiobserver/_version.py +1 -0
  7. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/fastapi.py +7 -0
  8. fastapi_observer-1.5.0/src/fastapiobserver/logging/filters.py +61 -0
  9. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/logging/formatter.py +9 -4
  10. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/logging/queueing.py +11 -0
  11. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/metrics.py +6 -2
  12. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/request_logging.py +31 -19
  13. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/tracing.py +0 -14
  14. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/plugins.py +3 -1
  15. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/security/normalize.py +4 -0
  16. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/security/proxies.py +15 -10
  17. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/security/redaction.py +37 -20
  18. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_fastapi_install.py +12 -0
  19. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_logging_schema_contract.py +28 -0
  20. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otel_log_correlation.py +7 -0
  21. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_trust_boundary.py +22 -1
  22. fastapi_observer-1.4.0/src/fastapiobserver/_version.py +0 -1
  23. fastapi_observer-1.4.0/src/fastapiobserver/logging/filters.py +0 -53
  24. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/LICENSE +0 -0
  25. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/setup.cfg +0 -0
  26. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapi_observer.egg-info/SOURCES.txt +0 -0
  27. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
  28. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
  29. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/__init__.py +0 -0
  30. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/audit/__init__.py +0 -0
  31. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/audit/formatter.py +0 -0
  32. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/audit/providers.py +0 -0
  33. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/audit/verify.py +0 -0
  34. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/config.py +0 -0
  35. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/control_plane.py +0 -0
  36. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/db_tracing.py +0 -0
  37. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/integrations/__init__.py +0 -0
  38. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
  39. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/logging/__init__.py +0 -0
  40. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/logging/circuit_breaker.py +0 -0
  41. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/logging/setup.py +0 -0
  42. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/logging/state.py +0 -0
  43. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/loguru.py +0 -0
  44. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/__init__.py +0 -0
  45. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/builder.py +0 -0
  46. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/contracts.py +0 -0
  47. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/endpoint.py +0 -0
  48. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/noop.py +0 -0
  49. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/pathing.py +0 -0
  50. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/prometheus/__init__.py +0 -0
  51. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/prometheus/backend.py +0 -0
  52. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/prometheus/client.py +0 -0
  53. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/prometheus/collector.py +0 -0
  54. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/prometheus/exemplars.py +0 -0
  55. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/prometheus/multiprocess.py +0 -0
  56. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/metrics/registry.py +0 -0
  57. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/__init__.py +0 -0
  58. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/body_capture.py +0 -0
  59. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/context.py +0 -0
  60. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/events.py +0 -0
  61. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/headers.py +0 -0
  62. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/ip.py +0 -0
  63. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/middleware/span_errors.py +0 -0
  64. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/__init__.py +0 -0
  65. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/exclusions.py +0 -0
  66. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/lifecycle.py +0 -0
  67. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/logs.py +0 -0
  68. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/metrics.py +0 -0
  69. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/resource.py +0 -0
  70. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/settings.py +0 -0
  71. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/otel/types.py +0 -0
  72. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/profiles.py +0 -0
  73. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/propagation.py +0 -0
  74. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/py.typed +0 -0
  75. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/request_context.py +0 -0
  76. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/security/__init__.py +0 -0
  77. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/security/policy.py +0 -0
  78. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/__init__.py +0 -0
  79. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/builtin.py +0 -0
  80. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/discovery.py +0 -0
  81. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/factory.py +0 -0
  82. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/logtail/__init__.py +0 -0
  83. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/logtail/dlq.py +0 -0
  84. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/logtail/handler.py +0 -0
  85. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/logtail/sink.py +0 -0
  86. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/protocol.py +0 -0
  87. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/registry.py +0 -0
  88. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/sinks/stats.py +0 -0
  89. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/src/fastapiobserver/utils.py +0 -0
  90. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_audit_chain.py +0 -0
  91. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_body_media_allowlist.py +0 -0
  92. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_config_settings.py +0 -0
  93. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_db_tracing.py +0 -0
  94. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_dx_parity.py +0 -0
  95. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_excluded_url_precedence.py +0 -0
  96. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_fork_queue_listener.py +0 -0
  97. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_graphql_integrations.py +0 -0
  98. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_lifespan_teardown.py +0 -0
  99. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_log_queue_controls.py +0 -0
  100. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_logtail_dlq.py +0 -0
  101. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_loguru_bridge.py +0 -0
  102. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_metrics_contracts.py +0 -0
  103. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_metrics_optional_dependency.py +0 -0
  104. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_middleware.py +0 -0
  105. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_negotiate_content_type.py +0 -0
  106. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otel_integration.py +0 -0
  107. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otel_lifecycle.py +0 -0
  108. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otel_logs_from_env.py +0 -0
  109. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otel_logs_pipeline.py +0 -0
  110. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otel_metrics_from_env.py +0 -0
  111. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_otlp_export_integration.py +0 -0
  112. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_plugin_isolation.py +0 -0
  113. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_profiles.py +0 -0
  114. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_propagation.py +0 -0
  115. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_redaction_presets.py +0 -0
  116. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_route_template_cardinality.py +0 -0
  117. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_runtime_control_auth.py +0 -0
  118. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_security_audit_fixes.py +0 -0
  119. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_security_redaction.py +0 -0
  120. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_sink_circuit_breaker.py +0 -0
  121. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_sinks.py +0 -0
  122. {fastapi_observer-1.4.0 → fastapi_observer-1.5.0}/tests/test_utils_refactors.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-observer
3
- Version: 1.4.0
3
+ Version: 1.5.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
7
7
  Project-URL: Homepage, https://github.com/Vitaee/FastapiObserver
8
- Project-URL: Documentation, https://github.com/Vitaee/FastapiObserver#readme
8
+ Project-URL: Documentation, https://vitaee.github.io/FastapiObserver/
9
9
  Project-URL: Repository, https://github.com/Vitaee/FastapiObserver.git
10
10
  Project-URL: Issues, https://github.com/Vitaee/FastapiObserver/issues
11
11
  Project-URL: Funding, https://buymeacoffee.com/FYbPCSu
@@ -14,7 +14,6 @@ Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
@@ -24,7 +23,7 @@ Classifier: Framework :: FastAPI
24
23
  Classifier: Topic :: System :: Logging
25
24
  Classifier: Topic :: System :: Monitoring
26
25
  Classifier: Typing :: Typed
27
- Requires-Python: <3.16,>=3.10
26
+ Requires-Python: >=3.11
28
27
  Description-Content-Type: text/markdown
29
28
  License-File: LICENSE
30
29
  Requires-Dist: fastapi>=0.133.0
@@ -64,8 +63,7 @@ Requires-Dist: opentelemetry-instrumentation-requests>=0.66b1; extra == "all"
64
63
  Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.66b1; extra == "all"
65
64
  Requires-Dist: cryptography>=50.0.0; extra == "all"
66
65
  Provides-Extra: dev
67
- Requires-Dist: httpx>=0.28.1; python_version < "3.11" and extra == "dev"
68
- Requires-Dist: httpx2>=2.16.0; python_version >= "3.11" and extra == "dev"
66
+ Requires-Dist: httpx2>=2.16.0; extra == "dev"
69
67
  Requires-Dist: loguru>=0.7.3; extra == "dev"
70
68
  Requires-Dist: mypy>=2.4.1; extra == "dev"
71
69
  Requires-Dist: pip-audit>=2.10.1; extra == "dev"
@@ -85,7 +83,9 @@ Dynamic: license-file
85
83
 
86
84
  `fastapi-observer` gives you structured JSON logs, request correlation, Prometheus metrics, OpenTelemetry tracing, security redaction presets, and runtime controls in one install step and one function call.
87
85
 
88
- **Supported Python versions:** `3.10` to `3.15`
86
+ **Documentation:** [vitaee.github.io/FastapiObserver](https://vitaee.github.io/FastapiObserver/)
87
+
88
+ **Supported Python versions:** `3.11` and newer (CI-tested on `3.11` to `3.15`)
89
89
 
90
90
  ---
91
91
 
@@ -94,7 +94,7 @@ Dynamic: license-file
94
94
 
95
95
  | Component | Supported / Tested |
96
96
  |---|---|
97
- | Python | `3.10` to `3.15` (CI matrix) |
97
+ | Python | `>=3.11` (CI matrix: `3.11` to `3.15`) |
98
98
  | FastAPI | `>=0.133.0` |
99
99
  | Starlette | `>=1.3.1` |
100
100
  | pydantic-settings | `>=2.10.1` |
@@ -233,17 +233,17 @@ To dramatically reduce boilerplate, you can use the `OBS_PROFILE` environment va
233
233
 
234
234
  ## Documentation Map
235
235
 
236
- For deep-dive documentation, read the `docs/` folder:
237
- - [Security & Presets](docs/security.md)
238
- - [Environment Variables](docs/configuration.md)
239
- - [Runtime Control](docs/runtime-control.md)
240
- - [OpenTelemetry](docs/opentelemetry.md)
241
- - [Logtail Sink](docs/logtail-sink.md)
242
- - [Audit Logging](docs/audit-logging.md)
243
- - [Database Tracing](docs/db-tracing.md)
244
- - [GraphQL Support](docs/graphql.md)
245
- - [Architecture & Operations](docs/architecture.md)
246
- - [Production Deployment](docs/deployment.md)
247
- - [Performance Tuning](docs/tuning.md)
248
- - [Advanced Operations](docs/advanced.md)
249
- - [Maintenance & Contributing](docs/contributing.md) (quality gates, release flow, runtime stress harness)
236
+ Full documentation lives at [vitaee.github.io/FastapiObserver](https://vitaee.github.io/FastapiObserver/). Deep dives:
237
+ - [Security & Presets](https://vitaee.github.io/FastapiObserver/security/)
238
+ - [Environment Variables](https://vitaee.github.io/FastapiObserver/configuration/)
239
+ - [Runtime Control](https://vitaee.github.io/FastapiObserver/runtime-control/)
240
+ - [OpenTelemetry](https://vitaee.github.io/FastapiObserver/opentelemetry/)
241
+ - [Logtail Sink](https://vitaee.github.io/FastapiObserver/logtail-sink/)
242
+ - [Audit Logging](https://vitaee.github.io/FastapiObserver/audit-logging/)
243
+ - [Database Tracing](https://vitaee.github.io/FastapiObserver/db-tracing/)
244
+ - [GraphQL Support](https://vitaee.github.io/FastapiObserver/graphql/)
245
+ - [Architecture & Operations](https://vitaee.github.io/FastapiObserver/architecture/)
246
+ - [Production Deployment](https://vitaee.github.io/FastapiObserver/deployment/)
247
+ - [Performance Tuning](https://vitaee.github.io/FastapiObserver/tuning/)
248
+ - [Advanced Operations](https://vitaee.github.io/FastapiObserver/advanced/)
249
+ - [Maintenance & Contributing](https://vitaee.github.io/FastapiObserver/contributing/) (quality gates, release flow, runtime stress harness)
@@ -7,7 +7,9 @@
7
7
 
8
8
  `fastapi-observer` gives you structured JSON logs, request correlation, Prometheus metrics, OpenTelemetry tracing, security redaction presets, and runtime controls in one install step and one function call.
9
9
 
10
- **Supported Python versions:** `3.10` to `3.15`
10
+ **Documentation:** [vitaee.github.io/FastapiObserver](https://vitaee.github.io/FastapiObserver/)
11
+
12
+ **Supported Python versions:** `3.11` and newer (CI-tested on `3.11` to `3.15`)
11
13
 
12
14
  ---
13
15
 
@@ -16,7 +18,7 @@
16
18
 
17
19
  | Component | Supported / Tested |
18
20
  |---|---|
19
- | Python | `3.10` to `3.15` (CI matrix) |
21
+ | Python | `>=3.11` (CI matrix: `3.11` to `3.15`) |
20
22
  | FastAPI | `>=0.133.0` |
21
23
  | Starlette | `>=1.3.1` |
22
24
  | pydantic-settings | `>=2.10.1` |
@@ -155,17 +157,17 @@ To dramatically reduce boilerplate, you can use the `OBS_PROFILE` environment va
155
157
 
156
158
  ## Documentation Map
157
159
 
158
- For deep-dive documentation, read the `docs/` folder:
159
- - [Security & Presets](docs/security.md)
160
- - [Environment Variables](docs/configuration.md)
161
- - [Runtime Control](docs/runtime-control.md)
162
- - [OpenTelemetry](docs/opentelemetry.md)
163
- - [Logtail Sink](docs/logtail-sink.md)
164
- - [Audit Logging](docs/audit-logging.md)
165
- - [Database Tracing](docs/db-tracing.md)
166
- - [GraphQL Support](docs/graphql.md)
167
- - [Architecture & Operations](docs/architecture.md)
168
- - [Production Deployment](docs/deployment.md)
169
- - [Performance Tuning](docs/tuning.md)
170
- - [Advanced Operations](docs/advanced.md)
171
- - [Maintenance & Contributing](docs/contributing.md) (quality gates, release flow, runtime stress harness)
160
+ Full documentation lives at [vitaee.github.io/FastapiObserver](https://vitaee.github.io/FastapiObserver/). Deep dives:
161
+ - [Security & Presets](https://vitaee.github.io/FastapiObserver/security/)
162
+ - [Environment Variables](https://vitaee.github.io/FastapiObserver/configuration/)
163
+ - [Runtime Control](https://vitaee.github.io/FastapiObserver/runtime-control/)
164
+ - [OpenTelemetry](https://vitaee.github.io/FastapiObserver/opentelemetry/)
165
+ - [Logtail Sink](https://vitaee.github.io/FastapiObserver/logtail-sink/)
166
+ - [Audit Logging](https://vitaee.github.io/FastapiObserver/audit-logging/)
167
+ - [Database Tracing](https://vitaee.github.io/FastapiObserver/db-tracing/)
168
+ - [GraphQL Support](https://vitaee.github.io/FastapiObserver/graphql/)
169
+ - [Architecture & Operations](https://vitaee.github.io/FastapiObserver/architecture/)
170
+ - [Production Deployment](https://vitaee.github.io/FastapiObserver/deployment/)
171
+ - [Performance Tuning](https://vitaee.github.io/FastapiObserver/tuning/)
172
+ - [Advanced Operations](https://vitaee.github.io/FastapiObserver/advanced/)
173
+ - [Maintenance & Contributing](https://vitaee.github.io/FastapiObserver/contributing/) (quality gates, release flow, runtime stress harness)
@@ -7,7 +7,7 @@ name = "fastapi-observer"
7
7
  dynamic = ["version"]
8
8
  description = "Zero-glue FastAPI observability with security presets and runtime controls"
9
9
  readme = "README.md"
10
- requires-python = ">=3.10,<3.16"
10
+ requires-python = ">=3.11"
11
11
  authors = [
12
12
  { name = "Vitaee", email = "opensource@vitaee.dev" },
13
13
  ]
@@ -16,7 +16,6 @@ classifiers = [
16
16
  "Intended Audience :: Developers",
17
17
  "Operating System :: OS Independent",
18
18
  "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.10",
20
19
  "Programming Language :: Python :: 3.11",
21
20
  "Programming Language :: Python :: 3.12",
22
21
  "Programming Language :: Python :: 3.13",
@@ -38,7 +37,7 @@ dependencies = [
38
37
 
39
38
  [project.urls]
40
39
  Homepage = "https://github.com/Vitaee/FastapiObserver"
41
- Documentation = "https://github.com/Vitaee/FastapiObserver#readme"
40
+ Documentation = "https://vitaee.github.io/FastapiObserver/"
42
41
  Repository = "https://github.com/Vitaee/FastapiObserver.git"
43
42
  Issues = "https://github.com/Vitaee/FastapiObserver/issues"
44
43
  Funding = "https://buymeacoffee.com/FYbPCSu"
@@ -87,8 +86,7 @@ all = [
87
86
  "cryptography>=50.0.0",
88
87
  ]
89
88
  dev = [
90
- "httpx>=0.28.1; python_version < '3.11'",
91
- "httpx2>=2.16.0; python_version >= '3.11'",
89
+ "httpx2>=2.16.0",
92
90
  "loguru>=0.7.3",
93
91
  "mypy>=2.4.1",
94
92
  "pip-audit>=2.10.1",
@@ -109,7 +107,7 @@ version = { attr = "fastapiobserver._version.__version__" }
109
107
  where = ["src"]
110
108
 
111
109
  [tool.ruff]
112
- target-version = "py310"
110
+ target-version = "py311"
113
111
  line-length = 100
114
112
  src = ["src", "tests"]
115
113
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-observer
3
- Version: 1.4.0
3
+ Version: 1.5.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
7
7
  Project-URL: Homepage, https://github.com/Vitaee/FastapiObserver
8
- Project-URL: Documentation, https://github.com/Vitaee/FastapiObserver#readme
8
+ Project-URL: Documentation, https://vitaee.github.io/FastapiObserver/
9
9
  Project-URL: Repository, https://github.com/Vitaee/FastapiObserver.git
10
10
  Project-URL: Issues, https://github.com/Vitaee/FastapiObserver/issues
11
11
  Project-URL: Funding, https://buymeacoffee.com/FYbPCSu
@@ -14,7 +14,6 @@ Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
@@ -24,7 +23,7 @@ Classifier: Framework :: FastAPI
24
23
  Classifier: Topic :: System :: Logging
25
24
  Classifier: Topic :: System :: Monitoring
26
25
  Classifier: Typing :: Typed
27
- Requires-Python: <3.16,>=3.10
26
+ Requires-Python: >=3.11
28
27
  Description-Content-Type: text/markdown
29
28
  License-File: LICENSE
30
29
  Requires-Dist: fastapi>=0.133.0
@@ -64,8 +63,7 @@ Requires-Dist: opentelemetry-instrumentation-requests>=0.66b1; extra == "all"
64
63
  Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.66b1; extra == "all"
65
64
  Requires-Dist: cryptography>=50.0.0; extra == "all"
66
65
  Provides-Extra: dev
67
- Requires-Dist: httpx>=0.28.1; python_version < "3.11" and extra == "dev"
68
- Requires-Dist: httpx2>=2.16.0; python_version >= "3.11" and extra == "dev"
66
+ Requires-Dist: httpx2>=2.16.0; extra == "dev"
69
67
  Requires-Dist: loguru>=0.7.3; extra == "dev"
70
68
  Requires-Dist: mypy>=2.4.1; extra == "dev"
71
69
  Requires-Dist: pip-audit>=2.10.1; extra == "dev"
@@ -85,7 +83,9 @@ Dynamic: license-file
85
83
 
86
84
  `fastapi-observer` gives you structured JSON logs, request correlation, Prometheus metrics, OpenTelemetry tracing, security redaction presets, and runtime controls in one install step and one function call.
87
85
 
88
- **Supported Python versions:** `3.10` to `3.15`
86
+ **Documentation:** [vitaee.github.io/FastapiObserver](https://vitaee.github.io/FastapiObserver/)
87
+
88
+ **Supported Python versions:** `3.11` and newer (CI-tested on `3.11` to `3.15`)
89
89
 
90
90
  ---
91
91
 
@@ -94,7 +94,7 @@ Dynamic: license-file
94
94
 
95
95
  | Component | Supported / Tested |
96
96
  |---|---|
97
- | Python | `3.10` to `3.15` (CI matrix) |
97
+ | Python | `>=3.11` (CI matrix: `3.11` to `3.15`) |
98
98
  | FastAPI | `>=0.133.0` |
99
99
  | Starlette | `>=1.3.1` |
100
100
  | pydantic-settings | `>=2.10.1` |
@@ -233,17 +233,17 @@ To dramatically reduce boilerplate, you can use the `OBS_PROFILE` environment va
233
233
 
234
234
  ## Documentation Map
235
235
 
236
- For deep-dive documentation, read the `docs/` folder:
237
- - [Security & Presets](docs/security.md)
238
- - [Environment Variables](docs/configuration.md)
239
- - [Runtime Control](docs/runtime-control.md)
240
- - [OpenTelemetry](docs/opentelemetry.md)
241
- - [Logtail Sink](docs/logtail-sink.md)
242
- - [Audit Logging](docs/audit-logging.md)
243
- - [Database Tracing](docs/db-tracing.md)
244
- - [GraphQL Support](docs/graphql.md)
245
- - [Architecture & Operations](docs/architecture.md)
246
- - [Production Deployment](docs/deployment.md)
247
- - [Performance Tuning](docs/tuning.md)
248
- - [Advanced Operations](docs/advanced.md)
249
- - [Maintenance & Contributing](docs/contributing.md) (quality gates, release flow, runtime stress harness)
236
+ Full documentation lives at [vitaee.github.io/FastapiObserver](https://vitaee.github.io/FastapiObserver/). Deep dives:
237
+ - [Security & Presets](https://vitaee.github.io/FastapiObserver/security/)
238
+ - [Environment Variables](https://vitaee.github.io/FastapiObserver/configuration/)
239
+ - [Runtime Control](https://vitaee.github.io/FastapiObserver/runtime-control/)
240
+ - [OpenTelemetry](https://vitaee.github.io/FastapiObserver/opentelemetry/)
241
+ - [Logtail Sink](https://vitaee.github.io/FastapiObserver/logtail-sink/)
242
+ - [Audit Logging](https://vitaee.github.io/FastapiObserver/audit-logging/)
243
+ - [Database Tracing](https://vitaee.github.io/FastapiObserver/db-tracing/)
244
+ - [GraphQL Support](https://vitaee.github.io/FastapiObserver/graphql/)
245
+ - [Architecture & Operations](https://vitaee.github.io/FastapiObserver/architecture/)
246
+ - [Production Deployment](https://vitaee.github.io/FastapiObserver/deployment/)
247
+ - [Performance Tuning](https://vitaee.github.io/FastapiObserver/tuning/)
248
+ - [Advanced Operations](https://vitaee.github.io/FastapiObserver/advanced/)
249
+ - [Maintenance & Contributing](https://vitaee.github.io/FastapiObserver/contributing/) (quality gates, release flow, runtime stress harness)
@@ -20,6 +20,7 @@ cryptography>=50.0.0
20
20
  cryptography>=50.0.0
21
21
 
22
22
  [dev]
23
+ httpx2>=2.16.0
23
24
  loguru>=0.7.3
24
25
  mypy>=2.4.1
25
26
  pip-audit>=2.10.1
@@ -29,12 +30,6 @@ ruff>=0.17.3
29
30
  cyclonedx-bom>=7.6.0
30
31
  uvicorn>=0.54.0
31
32
 
32
- [dev:python_version < "3.11"]
33
- httpx>=0.28.1
34
-
35
- [dev:python_version >= "3.11"]
36
- httpx2>=2.16.0
37
-
38
33
  [fast-json]
39
34
  orjson>=3.11.7
40
35
 
@@ -0,0 +1 @@
1
+ __version__ = "1.5.0"
@@ -126,6 +126,13 @@ def install_observability(
126
126
  metrics_backend,
127
127
  )
128
128
 
129
+ # --- 7. Uvicorn access log ---
130
+ # The middleware's `request.completed` line carries the same fields, so drop
131
+ # uvicorn's duplicate line the same way `uvicorn --no-access-log` does.
132
+ uvicorn_access_logger = logging.getLogger("uvicorn.access")
133
+ uvicorn_access_logger.handlers = []
134
+ uvicorn_access_logger.propagate = False
135
+
129
136
  # --- 8. Runtime control plane ---
130
137
  if runtime_control_settings and runtime_control_settings.enabled:
131
138
  mount_control_plane(app, runtime_control_settings)
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import sys
5
+
6
+ from ..plugins import apply_log_filters
7
+ from ..request_context import get_request_id, get_span_id, get_trace_id, get_user_context
8
+
9
+ _LOGGER = logging.getLogger("fastapiobserver.logging")
10
+
11
+
12
+ class RequestIdFilter(logging.Filter):
13
+ """Attach request ID and user context from context vars to every log record.
14
+
15
+ Records are formatted on the queue listener thread, where the request's
16
+ context vars are not visible, so they are captured here.
17
+ """
18
+
19
+ def filter(self, record: logging.LogRecord) -> bool:
20
+ if not getattr(record, "request_id", None):
21
+ record.request_id = get_request_id()
22
+ if not getattr(record, "user_context", None):
23
+ record.user_context = get_user_context()
24
+ return True
25
+
26
+
27
+ class TraceContextFilter(logging.Filter):
28
+ """Attach OTel trace_id / span_id to every log record."""
29
+
30
+ def filter(self, record: logging.LogRecord) -> bool:
31
+ trace_id = get_trace_id()
32
+ span_id = get_span_id()
33
+
34
+ # No span can be active unless something already imported the OTel API.
35
+ # Looking it up avoids a failing import (a sys.path scan) on every record.
36
+ otel_trace = sys.modules.get("opentelemetry.trace")
37
+ if otel_trace is not None:
38
+ try:
39
+ span_context = otel_trace.get_current_span().get_span_context()
40
+ if span_context and span_context.is_valid:
41
+ trace_id = f"{span_context.trace_id:032x}"
42
+ span_id = f"{span_context.span_id:016x}"
43
+ except Exception:
44
+ _LOGGER.debug(
45
+ "trace_context_filter.otel_lookup_failed",
46
+ exc_info=True,
47
+ extra={"_skip_enrichers": True},
48
+ )
49
+
50
+ if trace_id:
51
+ record.trace_id = trace_id # type: ignore[attr-defined]
52
+ if span_id:
53
+ record.span_id = span_id # type: ignore[attr-defined]
54
+ return True
55
+
56
+
57
+ class PluginLogFilter(logging.Filter):
58
+ """Apply user-registered log filters with fault isolation."""
59
+
60
+ def filter(self, record: logging.LogRecord) -> bool:
61
+ return apply_log_filters(record)
@@ -52,7 +52,7 @@ class StructuredJsonFormatter(logging.Formatter):
52
52
 
53
53
  def format(self, record: logging.LogRecord) -> str:
54
54
  payload: dict[str, Any] = {
55
- "timestamp": datetime.now(timezone.utc).isoformat(),
55
+ "timestamp": datetime.fromtimestamp(record.created, timezone.utc).isoformat(),
56
56
  "level": record.levelname,
57
57
  "logger": record.name,
58
58
  "message": record.getMessage(),
@@ -77,7 +77,7 @@ class StructuredJsonFormatter(logging.Formatter):
77
77
  if span_id:
78
78
  payload["span_id"] = span_id
79
79
 
80
- user_context = get_user_context()
80
+ user_context = getattr(record, "user_context", None) or get_user_context()
81
81
  if user_context:
82
82
  payload["user_context"] = user_context
83
83
 
@@ -96,17 +96,22 @@ class StructuredJsonFormatter(logging.Formatter):
96
96
  else:
97
97
  candidate_payload = self._enrich_event(dict(payload))
98
98
  if isinstance(candidate_payload, Mapping):
99
- enriched_payload = dict(candidate_payload)
99
+ enriched_payload = _as_dict(candidate_payload)
100
100
  else:
101
101
  enriched_payload = payload
102
102
  sanitized_candidate = self._sanitize_payload(enriched_payload, self.security_policy)
103
103
  if isinstance(sanitized_candidate, Mapping):
104
- sanitized_payload = dict(sanitized_candidate)
104
+ sanitized_payload = _as_dict(sanitized_candidate)
105
105
  else:
106
106
  sanitized_payload = enriched_payload
107
107
  return _json_dumps(sanitized_payload)
108
108
 
109
109
 
110
+ def _as_dict(value: Mapping[str, Any]) -> dict[str, Any]:
111
+ # Both steps already return fresh dicts; only copy other mapping types.
112
+ return value if type(value) is dict else dict(value)
113
+
114
+
110
115
  def _json_dumps(payload: dict[str, Any]) -> str:
111
116
  if orjson is not None:
112
117
  return orjson.dumps(payload).decode("utf-8")
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
3
4
  import logging
4
5
  import logging.handlers
5
6
  import queue
@@ -135,6 +136,16 @@ class OverflowPolicyQueueHandler(logging.handlers.QueueHandler):
135
136
  self.block_timeout_seconds = block_timeout_seconds
136
137
  self.telemetry = telemetry
137
138
 
139
+ def prepare(self, record: logging.LogRecord) -> logging.LogRecord:
140
+ # The stdlib version formats the record here and drops exc_info, which
141
+ # loses the structured `error` field. The queue stays in-process, so keep
142
+ # exc_info and leave formatting to the listener thread.
143
+ record = copy.copy(record)
144
+ record.message = record.getMessage()
145
+ record.msg = record.message
146
+ record.args = None
147
+ return record
148
+
138
149
  def enqueue(self, record: logging.LogRecord) -> None:
139
150
  try:
140
151
  self.queue.put_nowait(record)
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  import logging
4
4
 
5
5
  from ..config import ObservabilitySettings
6
- from ..metrics import MetricsBackend, collapse_dynamic_segments
7
- from ..plugins import emit_metric_hooks
6
+ from ..metrics import MetricsBackend, NoopMetricsBackend, collapse_dynamic_segments
7
+ from ..plugins import _METRIC_HOOKS, emit_metric_hooks
8
8
  from starlette.requests import Request
9
9
  from starlette.responses import Response
10
10
  from starlette.types import Scope
@@ -30,6 +30,8 @@ class _MetricsRecorder:
30
30
  duration_seconds: float,
31
31
  scope: Scope | None = None,
32
32
  ) -> None:
33
+ if isinstance(self.metrics_backend, NoopMetricsBackend):
34
+ return
33
35
  collapsed_path = collapse_dynamic_segments(path)
34
36
  excluded_urls = self.settings.metrics_exclude_paths
35
37
 
@@ -67,6 +69,8 @@ class _MetricsRecorder:
67
69
  status_code: int,
68
70
  duration_seconds: float,
69
71
  ) -> None:
72
+ if not _METRIC_HOOKS:
73
+ return
70
74
  request = Request(scope)
71
75
  response = Response(status_code=status_code)
72
76
  emit_metric_hooks(request, response, duration_seconds)
@@ -60,9 +60,8 @@ class RequestLoggingMiddleware:
60
60
  method = scope.get("method", "UNKNOWN")
61
61
  status_code = 500
62
62
  captured_error: Exception | None = None
63
- request_content_type = _get_header(raw_headers, b"content-type")
64
63
  request_body_enabled = self.security_policy.log_request_body and is_body_capturable(
65
- request_content_type,
64
+ _get_header(raw_headers, b"content-type"),
66
65
  self.security_policy,
67
66
  )
68
67
  request_body_capture = _BodyCapture(
@@ -102,7 +101,11 @@ class RequestLoggingMiddleware:
102
101
  await send(message)
103
102
 
104
103
  try:
105
- await self.app(scope, receive_wrapper, send_wrapper)
104
+ await self.app(
105
+ scope,
106
+ receive_wrapper if request_body_enabled else receive,
107
+ send_wrapper,
108
+ )
106
109
  except Exception as exc:
107
110
  had_error = True
108
111
  captured_error = exc
@@ -137,26 +140,35 @@ class RequestLoggingMiddleware:
137
140
  had_error: bool,
138
141
  ) -> None:
139
142
  duration_seconds = max(0.0, time.perf_counter() - start)
140
- error_type = _classify_error(status_code, captured_error)
141
- safe_event = self.event_builder.build(
142
- method=method,
143
- path=path,
144
- status_code=status_code,
145
- duration_seconds=duration_seconds,
146
- client_ip=client_ip,
147
- request_body=request_body_capture.value,
148
- response_body=response_body_capture.value,
149
- error_type=error_type,
150
- exception=captured_error,
151
- )
152
143
  if had_error:
153
- self.logger.exception("request.failed", extra={"event": safe_event})
144
+ level, message = logging.ERROR, "request.failed"
154
145
  elif status_code >= 500:
155
- self.logger.error("request.server_error", extra={"event": safe_event})
146
+ level, message = logging.ERROR, "request.server_error"
156
147
  elif status_code >= 400:
157
- self.logger.warning("request.client_error", extra={"event": safe_event})
148
+ level, message = logging.WARNING, "request.client_error"
158
149
  else:
159
- self.logger.info("request.completed", extra={"event": safe_event})
150
+ level, message = logging.INFO, "request.completed"
151
+
152
+ # Building and redacting the event is the costliest step; skip it when
153
+ # the record would be dropped by the log level anyway.
154
+ if self.logger.isEnabledFor(level):
155
+ safe_event = self.event_builder.build(
156
+ method=method,
157
+ path=path,
158
+ status_code=status_code,
159
+ duration_seconds=duration_seconds,
160
+ client_ip=client_ip,
161
+ request_body=request_body_capture.value,
162
+ response_body=response_body_capture.value,
163
+ error_type=_classify_error(status_code, captured_error),
164
+ exception=captured_error,
165
+ )
166
+ self.logger.log(
167
+ level,
168
+ message,
169
+ exc_info=captured_error if had_error else None,
170
+ extra={"event": safe_event},
171
+ )
160
172
 
161
173
  # Prefer route template for bounded cardinality
162
174
  route_template = _extract_route_template(scope, path)
@@ -40,9 +40,6 @@ def install_otel(
40
40
  fastapi_instrumentor_module = import_otel_module(
41
41
  "opentelemetry.instrumentation.fastapi"
42
42
  )
43
- logging_instrumentor_module = import_otel_module(
44
- "opentelemetry.instrumentation.logging"
45
- )
46
43
 
47
44
  set_trace_sampling_ratio(otel_settings.trace_sampling_ratio)
48
45
  current_provider = trace_api.get_tracer_provider()
@@ -122,17 +119,6 @@ def install_otel(
122
119
  app, **instrument_kwargs
123
120
  )
124
121
 
125
- try:
126
- logging_instrumentor_module.LoggingInstrumentor().instrument(
127
- set_logging_format=False
128
- )
129
- except Exception:
130
- _LOGGER.debug(
131
- "otel.logging_instrumentor.failed",
132
- exc_info=True,
133
- extra={"_skip_enrichers": True},
134
- )
135
-
136
122
  if provider_owned:
137
123
  register_shutdown_hook(
138
124
  key=f"otel.tracer_provider.{id(tracer_provider)}",
@@ -74,6 +74,8 @@ def clear_plugins() -> None:
74
74
 
75
75
 
76
76
  def apply_log_enrichers(event: dict[str, object]) -> dict[str, object]:
77
+ if not _LOG_ENRICHERS:
78
+ return event
77
79
  with _LOCK:
78
80
  enrichers = list(_LOG_ENRICHERS.items())
79
81
 
@@ -92,7 +94,7 @@ def apply_log_enrichers(event: dict[str, object]) -> dict[str, object]:
92
94
 
93
95
 
94
96
  def apply_log_filters(record: logging.LogRecord) -> bool:
95
- if getattr(record, "_skip_log_filters", False):
97
+ if not _LOG_FILTERS or getattr(record, "_skip_log_filters", False):
96
98
  return True
97
99
 
98
100
  with _LOCK:
@@ -1,3 +1,7 @@
1
+ import functools
2
+
3
+
4
+ @functools.lru_cache(maxsize=1024)
1
5
  def _normalize_key(key: str) -> str:
2
6
  return key.strip().lower().replace("_", "-")
3
7
 
@@ -35,15 +35,20 @@ def resolve_client_ip(
35
35
  if not is_trusted_client_ip(client_ip, policy):
36
36
  return client_ip
37
37
 
38
- forwarded_for = None
39
- for k, v in headers:
40
- if k.lower() == b"x-forwarded-for":
41
- forwarded_for = v.decode("latin1")
42
- break
43
-
44
- if not forwarded_for:
45
- return client_ip
46
- first_hop = forwarded_for.split(",")[0].strip()
47
- return first_hop or client_ip
38
+ # Each proxy appends the address it received the request from, so only the
39
+ # right-hand entries can be trusted; the left ones are whatever the client sent.
40
+ # Walk right to left and return the first hop that is not a trusted proxy.
41
+ # Repeated header lines form one list.
42
+ hops = [
43
+ hop.strip()
44
+ for k, v in headers
45
+ if k.lower() == b"x-forwarded-for"
46
+ for hop in v.decode("latin1").split(",")
47
+ if hop.strip()
48
+ ]
49
+ for hop in reversed(hops):
50
+ if not is_trusted_client_ip(hop, policy):
51
+ return hop
52
+ return hops[0] if hops else client_ip
48
53
 
49
54
  __all__ = ["is_trusted_client_ip", "resolve_client_ip"]