fastapi-observer 1.12.0__tar.gz → 1.13.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.12.0/src/fastapi_observer.egg-info → fastapi_observer-1.13.0}/PKG-INFO +13 -11
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/README.md +12 -10
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0/src/fastapi_observer.egg-info}/PKG-INFO +13 -11
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapi_observer.egg-info/SOURCES.txt +2 -0
- fastapi_observer-1.13.0/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/fastapi.py +19 -11
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/metrics/prometheus.py +55 -7
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/request_logging.py +2 -0
- fastapi_observer-1.13.0/src/fastapiobserver/pools.py +99 -0
- fastapi_observer-1.13.0/tests/test_pools.py +116 -0
- fastapi_observer-1.12.0/src/fastapiobserver/_version.py +0 -1
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/LICENSE +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/pyproject.toml +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/setup.cfg +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/audit/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/audit/formatter.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/audit/providers.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/audit/verify.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/control_plane.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/db_tracing.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/circuit_breaker.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/filters.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/formatter.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/held.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/queueing.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/setup.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/state.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/volume.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/loop_watchdog.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/metrics/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/body_capture.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/context.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/events.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/headers.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/ip.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/metrics.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/exclusions.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/lifecycle.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/logs.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/metrics.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/native.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/resource.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/otel/tracing.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/profiles.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/request_context.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/normalize.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/policy.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/proxies.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/redaction.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/builtin.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/discovery.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/factory.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/__init__.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/dlq.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/handler.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/sink.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/protocol.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/registry.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/stats.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_audit_chain.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_config_settings.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_db_tracing.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_dx_parity.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_excluded_url_precedence.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_fork_queue_listener.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_lifespan_teardown.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_log_queue_controls.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_logging_schema_contract.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_loop_watchdog.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_metrics_contracts.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_metrics_optional_dependency.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_middleware.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_native_telemetry.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_negotiate_content_type.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otel_integration.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otel_lifecycle.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otel_log_correlation.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otel_logs_pipeline.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_otlp_export_integration.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_profiles.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_propagation.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_request_insights.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_route_template_cardinality.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_security_audit_fixes.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_sink_circuit_breaker.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_sinks.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_trust_boundary.py +0 -0
- {fastapi_observer-1.12.0 → fastapi_observer-1.13.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.13.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
|
|
@@ -150,9 +150,9 @@ Explicit variables and Python arguments always win over profile defaults. Every
|
|
|
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
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
|
-
- **OpenTelemetry traces
|
|
153
|
+
- **OpenTelemetry traces**, from FastAPI's own support on 0.142 and later, with `trace_id` and `span_id` in every log line, and optional log export over OTLP, redacted like everything else.
|
|
154
154
|
- **Non-blocking log writes.** Lines are written from a background thread, so a slow stdout never stalls the event loop.
|
|
155
|
-
- **Help finding what slows a request down.** Opt-in: a warning with the stack of any sync call that blocks the event loop, and `DEBUG` lines written only for requests that fail. With tracing on, each request line counts its queries and outbound HTTP calls, and a query run once per item (N+1) gets a warning. With metrics on, `/metrics` shows how many bytes of logs each route writes, and `LOG_SAMPLE_RATE` keeps a share of successful requests, chosen by trace ID, while failed and slow requests are always written in full.
|
|
155
|
+
- **Help finding what slows a request down.** Opt-in: a warning with the stack of any sync call that blocks the event loop, and `DEBUG` lines written only for requests that fail. Always on: a warning when the thread pool or a database connection pool is full, the usual cause when latency climbs while CPU stays low. With tracing on, each request line counts its queries and outbound HTTP calls, and a query run once per item (N+1) gets a warning. With metrics on, `/metrics` shows how many bytes of logs each route writes, and `LOG_SAMPLE_RATE` keeps a share of successful requests, chosen by trace ID, while failed and slow requests are always written in full.
|
|
156
156
|
|
|
157
157
|
Optional extras: runtime log-level control, a tamper-evident audit log, SQLAlchemy, `httpx` and `requests` tracing, Strawberry GraphQL and a Loguru bridge. See the [documentation](https://vitaee.github.io/FastapiObserver/).
|
|
158
158
|
|
|
@@ -162,14 +162,16 @@ Optional extras: runtime log-level control, a tamper-evident audit log, SQLAlche
|
|
|
162
162
|
|
|
163
163
|
## How it compares
|
|
164
164
|
|
|
165
|
-
| | fastapi-observer | structlog or loguru | OpenTelemetry auto-instrumentation | Logfire |
|
|
166
|
-
|
|
167
|
-
| JSON log line per request | Built in | You write the middleware | No | A span per request |
|
|
168
|
-
| Request ID on every log line | Built in | You wire it up | No | Trace IDs |
|
|
169
|
-
| Redaction by default | Yes, with presets | You write a processor | No | Yes |
|
|
170
|
-
| Prometheus `/metrics` | Built in | No | Through an exporter | No |
|
|
171
|
-
| Traces | OpenTelemetry | No | Yes | Yes |
|
|
172
|
-
| Where data goes | stdout, Prometheus and any OTLP backend | stdout or files | Any OTLP backend | Logfire's platform |
|
|
165
|
+
| | fastapi-observer | FastAPI 0.142+ built in | structlog or loguru | OpenTelemetry auto-instrumentation | Logfire |
|
|
166
|
+
|---|---|---|---|---|---|
|
|
167
|
+
| JSON log line per request | Built in | No, only errors as OTLP log records | You write the middleware | No | A span per request |
|
|
168
|
+
| Request ID on every log line | Built in | No | You wire it up | No | Trace IDs |
|
|
169
|
+
| Redaction by default | Yes, with presets | Signed-URL query parameters only | You write a processor | No | Yes |
|
|
170
|
+
| Prometheus `/metrics` | Built in | Through an exporter | No | Through an exporter | No |
|
|
171
|
+
| Traces | FastAPI's own on 0.142+, OpenTelemetry's instrumentation before | Yes, with spans for dependencies, the endpoint and serialization | No | Yes | Yes |
|
|
172
|
+
| Where data goes | stdout, Prometheus and any OTLP backend | Any OTLP backend | stdout or files | Any OTLP backend | Logfire's platform |
|
|
173
|
+
|
|
174
|
+
Since 0.142, FastAPI traces requests on its own once an OpenTelemetry provider is set up. fastapi-observer uses that for traces and adds the rest of the first column.
|
|
173
175
|
|
|
174
176
|
**When not to use it:**
|
|
175
177
|
|
|
@@ -74,9 +74,9 @@ Explicit variables and Python arguments always win over profile defaults. Every
|
|
|
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
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
|
-
- **OpenTelemetry traces
|
|
77
|
+
- **OpenTelemetry traces**, from FastAPI's own support on 0.142 and later, with `trace_id` and `span_id` in every log line, and optional log export over OTLP, redacted like everything else.
|
|
78
78
|
- **Non-blocking log writes.** Lines are written from a background thread, so a slow stdout never stalls the event loop.
|
|
79
|
-
- **Help finding what slows a request down.** Opt-in: a warning with the stack of any sync call that blocks the event loop, and `DEBUG` lines written only for requests that fail. With tracing on, each request line counts its queries and outbound HTTP calls, and a query run once per item (N+1) gets a warning. With metrics on, `/metrics` shows how many bytes of logs each route writes, and `LOG_SAMPLE_RATE` keeps a share of successful requests, chosen by trace ID, while failed and slow requests are always written in full.
|
|
79
|
+
- **Help finding what slows a request down.** Opt-in: a warning with the stack of any sync call that blocks the event loop, and `DEBUG` lines written only for requests that fail. Always on: a warning when the thread pool or a database connection pool is full, the usual cause when latency climbs while CPU stays low. With tracing on, each request line counts its queries and outbound HTTP calls, and a query run once per item (N+1) gets a warning. With metrics on, `/metrics` shows how many bytes of logs each route writes, and `LOG_SAMPLE_RATE` keeps a share of successful requests, chosen by trace ID, while failed and slow requests are always written in full.
|
|
80
80
|
|
|
81
81
|
Optional extras: runtime log-level control, a tamper-evident audit log, SQLAlchemy, `httpx` and `requests` tracing, Strawberry GraphQL and a Loguru bridge. See the [documentation](https://vitaee.github.io/FastapiObserver/).
|
|
82
82
|
|
|
@@ -86,14 +86,16 @@ Optional extras: runtime log-level control, a tamper-evident audit log, SQLAlche
|
|
|
86
86
|
|
|
87
87
|
## How it compares
|
|
88
88
|
|
|
89
|
-
| | fastapi-observer | structlog or loguru | OpenTelemetry auto-instrumentation | Logfire |
|
|
90
|
-
|
|
91
|
-
| JSON log line per request | Built in | You write the middleware | No | A span per request |
|
|
92
|
-
| Request ID on every log line | Built in | You wire it up | No | Trace IDs |
|
|
93
|
-
| Redaction by default | Yes, with presets | You write a processor | No | Yes |
|
|
94
|
-
| Prometheus `/metrics` | Built in | No | Through an exporter | No |
|
|
95
|
-
| Traces | OpenTelemetry | No | Yes | Yes |
|
|
96
|
-
| Where data goes | stdout, Prometheus and any OTLP backend | stdout or files | Any OTLP backend | Logfire's platform |
|
|
89
|
+
| | fastapi-observer | FastAPI 0.142+ built in | structlog or loguru | OpenTelemetry auto-instrumentation | Logfire |
|
|
90
|
+
|---|---|---|---|---|---|
|
|
91
|
+
| JSON log line per request | Built in | No, only errors as OTLP log records | You write the middleware | No | A span per request |
|
|
92
|
+
| Request ID on every log line | Built in | No | You wire it up | No | Trace IDs |
|
|
93
|
+
| Redaction by default | Yes, with presets | Signed-URL query parameters only | You write a processor | No | Yes |
|
|
94
|
+
| Prometheus `/metrics` | Built in | Through an exporter | No | Through an exporter | No |
|
|
95
|
+
| Traces | FastAPI's own on 0.142+, OpenTelemetry's instrumentation before | Yes, with spans for dependencies, the endpoint and serialization | No | Yes | Yes |
|
|
96
|
+
| Where data goes | stdout, Prometheus and any OTLP backend | Any OTLP backend | stdout or files | Any OTLP backend | Logfire's platform |
|
|
97
|
+
|
|
98
|
+
Since 0.142, FastAPI traces requests on its own once an OpenTelemetry provider is set up. fastapi-observer uses that for traces and adds the rest of the first column.
|
|
97
99
|
|
|
98
100
|
**When not to use it:**
|
|
99
101
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-observer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.13.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
|
|
@@ -150,9 +150,9 @@ Explicit variables and Python arguments always win over profile defaults. Every
|
|
|
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
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
|
-
- **OpenTelemetry traces
|
|
153
|
+
- **OpenTelemetry traces**, from FastAPI's own support on 0.142 and later, with `trace_id` and `span_id` in every log line, and optional log export over OTLP, redacted like everything else.
|
|
154
154
|
- **Non-blocking log writes.** Lines are written from a background thread, so a slow stdout never stalls the event loop.
|
|
155
|
-
- **Help finding what slows a request down.** Opt-in: a warning with the stack of any sync call that blocks the event loop, and `DEBUG` lines written only for requests that fail. With tracing on, each request line counts its queries and outbound HTTP calls, and a query run once per item (N+1) gets a warning. With metrics on, `/metrics` shows how many bytes of logs each route writes, and `LOG_SAMPLE_RATE` keeps a share of successful requests, chosen by trace ID, while failed and slow requests are always written in full.
|
|
155
|
+
- **Help finding what slows a request down.** Opt-in: a warning with the stack of any sync call that blocks the event loop, and `DEBUG` lines written only for requests that fail. Always on: a warning when the thread pool or a database connection pool is full, the usual cause when latency climbs while CPU stays low. With tracing on, each request line counts its queries and outbound HTTP calls, and a query run once per item (N+1) gets a warning. With metrics on, `/metrics` shows how many bytes of logs each route writes, and `LOG_SAMPLE_RATE` keeps a share of successful requests, chosen by trace ID, while failed and slow requests are always written in full.
|
|
156
156
|
|
|
157
157
|
Optional extras: runtime log-level control, a tamper-evident audit log, SQLAlchemy, `httpx` and `requests` tracing, Strawberry GraphQL and a Loguru bridge. See the [documentation](https://vitaee.github.io/FastapiObserver/).
|
|
158
158
|
|
|
@@ -162,14 +162,16 @@ Optional extras: runtime log-level control, a tamper-evident audit log, SQLAlche
|
|
|
162
162
|
|
|
163
163
|
## How it compares
|
|
164
164
|
|
|
165
|
-
| | fastapi-observer | structlog or loguru | OpenTelemetry auto-instrumentation | Logfire |
|
|
166
|
-
|
|
167
|
-
| JSON log line per request | Built in | You write the middleware | No | A span per request |
|
|
168
|
-
| Request ID on every log line | Built in | You wire it up | No | Trace IDs |
|
|
169
|
-
| Redaction by default | Yes, with presets | You write a processor | No | Yes |
|
|
170
|
-
| Prometheus `/metrics` | Built in | No | Through an exporter | No |
|
|
171
|
-
| Traces | OpenTelemetry | No | Yes | Yes |
|
|
172
|
-
| Where data goes | stdout, Prometheus and any OTLP backend | stdout or files | Any OTLP backend | Logfire's platform |
|
|
165
|
+
| | fastapi-observer | FastAPI 0.142+ built in | structlog or loguru | OpenTelemetry auto-instrumentation | Logfire |
|
|
166
|
+
|---|---|---|---|---|---|
|
|
167
|
+
| JSON log line per request | Built in | No, only errors as OTLP log records | You write the middleware | No | A span per request |
|
|
168
|
+
| Request ID on every log line | Built in | No | You wire it up | No | Trace IDs |
|
|
169
|
+
| Redaction by default | Yes, with presets | Signed-URL query parameters only | You write a processor | No | Yes |
|
|
170
|
+
| Prometheus `/metrics` | Built in | Through an exporter | No | Through an exporter | No |
|
|
171
|
+
| Traces | FastAPI's own on 0.142+, OpenTelemetry's instrumentation before | Yes, with spans for dependencies, the endpoint and serialization | No | Yes | Yes |
|
|
172
|
+
| Where data goes | stdout, Prometheus and any OTLP backend | Any OTLP backend | stdout or files | Any OTLP backend | Logfire's platform |
|
|
173
|
+
|
|
174
|
+
Since 0.142, FastAPI traces requests on its own once an OpenTelemetry provider is set up. fastapi-observer uses that for traces and adds the rest of the first column.
|
|
173
175
|
|
|
174
176
|
**When not to use it:**
|
|
175
177
|
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapi_observer.egg-info/SOURCES.txt
RENAMED
|
@@ -15,6 +15,7 @@ src/fastapiobserver/fastapi.py
|
|
|
15
15
|
src/fastapiobserver/loguru.py
|
|
16
16
|
src/fastapiobserver/loop_watchdog.py
|
|
17
17
|
src/fastapiobserver/plugins.py
|
|
18
|
+
src/fastapiobserver/pools.py
|
|
18
19
|
src/fastapiobserver/profiles.py
|
|
19
20
|
src/fastapiobserver/propagation.py
|
|
20
21
|
src/fastapiobserver/py.typed
|
|
@@ -98,6 +99,7 @@ tests/test_otel_logs_pipeline.py
|
|
|
98
99
|
tests/test_otel_metrics_from_env.py
|
|
99
100
|
tests/test_otlp_export_integration.py
|
|
100
101
|
tests/test_plugin_isolation.py
|
|
102
|
+
tests/test_pools.py
|
|
101
103
|
tests/test_profiles.py
|
|
102
104
|
tests/test_propagation.py
|
|
103
105
|
tests/test_redaction_presets.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.13.0"
|
|
@@ -35,6 +35,7 @@ from .otel import (
|
|
|
35
35
|
install_otel_metrics,
|
|
36
36
|
)
|
|
37
37
|
from .otel.native import keep_native_logs_out
|
|
38
|
+
from .pools import watch_db_pool, watch_thread_pool
|
|
38
39
|
from .propagation import install_request_id_propagation
|
|
39
40
|
from .security import SecurityPolicy, TrustedProxyPolicy
|
|
40
41
|
|
|
@@ -109,9 +110,13 @@ def install_observability(
|
|
|
109
110
|
if otel_settings and otel_settings.enabled:
|
|
110
111
|
install_otel(app, settings, otel_settings)
|
|
111
112
|
|
|
112
|
-
# --- 4b. Database tracing + SQLCommenter ---
|
|
113
|
-
if db_engine is not None
|
|
114
|
-
|
|
113
|
+
# --- 4b. Database pools, tracing + SQLCommenter ---
|
|
114
|
+
if db_engine is not None:
|
|
115
|
+
engines = _as_engine_list(db_engine)
|
|
116
|
+
for engine in engines:
|
|
117
|
+
watch_db_pool(getattr(engine, "sync_engine", engine))
|
|
118
|
+
if otel_settings and otel_settings.enabled:
|
|
119
|
+
_setup_database_tracing(engines, db_commenter_enabled, db_commenter_options)
|
|
115
120
|
|
|
116
121
|
# --- 5. OTel metrics ---
|
|
117
122
|
if otel_metrics_settings and otel_metrics_settings.enabled:
|
|
@@ -262,8 +267,17 @@ def _setup_metrics_backend(
|
|
|
262
267
|
return metrics_backend
|
|
263
268
|
|
|
264
269
|
|
|
270
|
+
def _as_engine_list(db_engine: Any | Sequence[Any]) -> list[Any]:
|
|
271
|
+
if isinstance(db_engine, (str, bytes, bytearray)):
|
|
272
|
+
raise TypeError(
|
|
273
|
+
f"db_engine must be an SQLAlchemy Engine or sequence of Engines, "
|
|
274
|
+
f"not {type(db_engine).__name__}"
|
|
275
|
+
)
|
|
276
|
+
return list(db_engine) if isinstance(db_engine, Sequence) else [db_engine]
|
|
277
|
+
|
|
278
|
+
|
|
265
279
|
def _setup_database_tracing(
|
|
266
|
-
|
|
280
|
+
engines: list[Any],
|
|
267
281
|
db_commenter_enabled: bool,
|
|
268
282
|
db_commenter_options: dict[str, bool] | None,
|
|
269
283
|
) -> None:
|
|
@@ -272,13 +286,6 @@ def _setup_database_tracing(
|
|
|
272
286
|
instrument_sqlalchemy_async,
|
|
273
287
|
)
|
|
274
288
|
|
|
275
|
-
if isinstance(db_engine, (str, bytes, bytearray)):
|
|
276
|
-
raise TypeError(
|
|
277
|
-
f"db_engine must be an SQLAlchemy Engine or sequence of Engines, "
|
|
278
|
-
f"not {type(db_engine).__name__}"
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
engines = db_engine if isinstance(db_engine, Sequence) else [db_engine]
|
|
282
289
|
for engine in engines:
|
|
283
290
|
if hasattr(engine, "sync_engine"):
|
|
284
291
|
instrument_sqlalchemy_async(
|
|
@@ -400,6 +407,7 @@ async def observability_lifespan(app: FastAPI) -> typing.AsyncGenerator[typing.A
|
|
|
400
407
|
watchdog = None
|
|
401
408
|
if state:
|
|
402
409
|
_auto_discover_excluded_routes(app, state.settings)
|
|
410
|
+
watch_thread_pool()
|
|
403
411
|
if state.settings.loop_block_threshold_ms:
|
|
404
412
|
watchdog = EventLoopWatchdog(state.settings.loop_block_threshold_ms / 1000)
|
|
405
413
|
watchdog.start()
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/metrics/prometheus.py
RENAMED
|
@@ -186,17 +186,65 @@ class _LogQueueMetricsCollector:
|
|
|
186
186
|
yield counter
|
|
187
187
|
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
class _PoolMetricsCollector:
|
|
190
|
+
"""Exposes thread pool and SQLAlchemy connection pool usage at scrape time."""
|
|
191
|
+
|
|
192
|
+
def collect(self) -> Iterator[Any]:
|
|
193
|
+
from prometheus_client.core import GaugeMetricFamily
|
|
194
|
+
|
|
195
|
+
from ..pools import db_pool_usage, thread_pool_usage
|
|
196
|
+
|
|
197
|
+
threads = thread_pool_usage()
|
|
198
|
+
if threads is not None:
|
|
199
|
+
busy, limit, waiting = threads
|
|
200
|
+
yield GaugeMetricFamily(
|
|
201
|
+
"fastapiobserver_threadpool_busy",
|
|
202
|
+
"Threads running sync endpoints and dependencies.",
|
|
203
|
+
value=busy,
|
|
204
|
+
)
|
|
205
|
+
yield GaugeMetricFamily(
|
|
206
|
+
"fastapiobserver_threadpool_limit",
|
|
207
|
+
"Threads AnyIO's default pool may run at once.",
|
|
208
|
+
value=limit,
|
|
209
|
+
)
|
|
210
|
+
yield GaugeMetricFamily(
|
|
211
|
+
"fastapiobserver_threadpool_waiting",
|
|
212
|
+
"Calls waiting for a thread.",
|
|
213
|
+
value=waiting,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
db_pools = db_pool_usage()
|
|
217
|
+
if not db_pools:
|
|
218
|
+
return
|
|
219
|
+
checked_out = GaugeMetricFamily(
|
|
220
|
+
"fastapiobserver_db_pool_checked_out",
|
|
221
|
+
"Connections checked out of each SQLAlchemy pool.",
|
|
222
|
+
labels=["engine"],
|
|
223
|
+
)
|
|
224
|
+
limits = GaugeMetricFamily(
|
|
225
|
+
"fastapiobserver_db_pool_limit",
|
|
226
|
+
"Connections each SQLAlchemy pool may hand out: pool_size plus max_overflow.",
|
|
227
|
+
labels=["engine"],
|
|
228
|
+
)
|
|
229
|
+
for label, out, limit in db_pools:
|
|
230
|
+
checked_out.add_metric([label], out)
|
|
231
|
+
limits.add_metric([label], limit)
|
|
232
|
+
yield checked_out
|
|
233
|
+
yield limits
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _register_collectors() -> None:
|
|
237
|
+
# Per-worker stats would be meaningless once aggregated across processes.
|
|
191
238
|
if _multiprocess_dir():
|
|
192
239
|
return
|
|
193
240
|
from ..logging.volume import start_counting
|
|
194
241
|
|
|
195
242
|
start_counting()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
243
|
+
for collector in (_LogQueueMetricsCollector(), _PoolMetricsCollector()):
|
|
244
|
+
try:
|
|
245
|
+
_import_prometheus_client().REGISTRY.register(collector)
|
|
246
|
+
except ValueError:
|
|
247
|
+
pass # Already registered by an earlier backend build in this process.
|
|
200
248
|
|
|
201
249
|
|
|
202
250
|
class PrometheusMetricsBackend:
|
|
@@ -288,7 +336,7 @@ def _build_prometheus_metrics_backend(
|
|
|
288
336
|
environment=environment,
|
|
289
337
|
exemplars_enabled=exemplars_enabled,
|
|
290
338
|
)
|
|
291
|
-
|
|
339
|
+
_register_collectors()
|
|
292
340
|
return backend
|
|
293
341
|
|
|
294
342
|
|
|
@@ -9,6 +9,7 @@ from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
|
9
9
|
from ..config import ObservabilitySettings
|
|
10
10
|
from ..logging.held import keep_request_logs
|
|
11
11
|
from ..metrics import MetricsBackend, NoopMetricsBackend
|
|
12
|
+
from ..pools import check_pools
|
|
12
13
|
from ..request_context import get_request_state
|
|
13
14
|
from ..security import (
|
|
14
15
|
SecurityPolicy,
|
|
@@ -62,6 +63,7 @@ class RequestLoggingMiddleware:
|
|
|
62
63
|
await self.app(scope, receive, send)
|
|
63
64
|
return
|
|
64
65
|
|
|
66
|
+
check_pools()
|
|
65
67
|
start = time.perf_counter()
|
|
66
68
|
raw_headers = scope.get("headers", [])
|
|
67
69
|
client_ip, trusted_source = self.ip_resolver.resolve(scope, raw_headers)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Full pools: requests wait, latency climbs, CPU stays low.
|
|
2
|
+
|
|
3
|
+
Two pools make requests wait without using CPU: AnyIO's thread pool, which runs
|
|
4
|
+
sync endpoints and dependencies, and SQLAlchemy connection pools. Neither AnyIO
|
|
5
|
+
nor SQLAlchemy has a hook for how long one call waited, so this reports when a
|
|
6
|
+
pool is full: a warning at most once a minute per pool, checked as each request
|
|
7
|
+
starts, and gauges on ``/metrics``. Requests still start when every thread and
|
|
8
|
+
connection is stuck, so a deadlock is reported too.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import math
|
|
15
|
+
import time
|
|
16
|
+
import weakref
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
_LOGGER = logging.getLogger("fastapiobserver.pools")
|
|
20
|
+
# ponytail: fixed; make it a setting if once a minute is too noisy or too quiet.
|
|
21
|
+
_WARN_INTERVAL_SECONDS = 60.0
|
|
22
|
+
|
|
23
|
+
_thread_limiter: Any = None # AnyIO's default limiter, captured on the event loop
|
|
24
|
+
_db_pools: weakref.WeakKeyDictionary[Any, str] = weakref.WeakKeyDictionary() # engine: label
|
|
25
|
+
_last_warned: dict[str, float] = {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def watch_thread_pool() -> None:
|
|
29
|
+
"""Remember AnyIO's default thread limiter. Call it on the event loop."""
|
|
30
|
+
global _thread_limiter
|
|
31
|
+
from anyio.to_thread import current_default_thread_limiter
|
|
32
|
+
|
|
33
|
+
_thread_limiter = current_default_thread_limiter()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def watch_db_pool(engine: Any) -> None:
|
|
37
|
+
"""Watch a sync SQLAlchemy engine's pool, if it has a limit (``QueuePool``)."""
|
|
38
|
+
if callable(getattr(engine.pool, "checkedout", None)):
|
|
39
|
+
_db_pools[engine] = engine.url.render_as_string(hide_password=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def thread_pool_usage() -> tuple[int, float, int] | None:
|
|
43
|
+
"""Busy threads, the limit and calls waiting; None until the app has started."""
|
|
44
|
+
if _thread_limiter is None:
|
|
45
|
+
return None
|
|
46
|
+
stats = _thread_limiter.statistics()
|
|
47
|
+
return stats.borrowed_tokens, stats.total_tokens, stats.tasks_waiting
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def db_pool_usage() -> list[tuple[str, int, float]]:
|
|
51
|
+
"""Each watched engine's label, checked-out connections and limit."""
|
|
52
|
+
return [
|
|
53
|
+
(label, engine.pool.checkedout(), _db_pool_limit(engine.pool))
|
|
54
|
+
for engine, label in list(_db_pools.items())
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_pools() -> None:
|
|
59
|
+
"""Warn about full pools. Called as each request starts."""
|
|
60
|
+
limiter = _thread_limiter
|
|
61
|
+
# Cheap test first; statistics() copies the borrower list.
|
|
62
|
+
if limiter is not None and limiter.borrowed_tokens >= limiter.total_tokens:
|
|
63
|
+
busy, limit, waiting = thread_pool_usage() or (0, 0, 0)
|
|
64
|
+
if waiting:
|
|
65
|
+
_warn(
|
|
66
|
+
"threadpool.saturated",
|
|
67
|
+
"threadpool",
|
|
68
|
+
{"busy": busy, "limit": limit, "waiting": waiting},
|
|
69
|
+
)
|
|
70
|
+
for label, checked_out, limit in db_pool_usage():
|
|
71
|
+
if checked_out >= limit:
|
|
72
|
+
_warn(
|
|
73
|
+
"db.pool_exhausted",
|
|
74
|
+
label,
|
|
75
|
+
{"engine": label, "checked_out": checked_out, "limit": limit},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _db_pool_limit(pool: Any) -> float:
|
|
80
|
+
# max_overflow has no public accessor; without it, or at -1, there's no limit.
|
|
81
|
+
max_overflow = getattr(pool, "_max_overflow", -1)
|
|
82
|
+
return math.inf if max_overflow < 0 else pool.size() + max_overflow
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _warn(message: str, pool: str, event: dict[str, Any]) -> None:
|
|
86
|
+
now = time.monotonic()
|
|
87
|
+
if now - _last_warned.get(pool, -math.inf) < _WARN_INTERVAL_SECONDS:
|
|
88
|
+
return
|
|
89
|
+
_last_warned[pool] = now
|
|
90
|
+
_LOGGER.warning(message, extra={"event": event, "_skip_enrichers": True})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"check_pools",
|
|
95
|
+
"db_pool_usage",
|
|
96
|
+
"thread_pool_usage",
|
|
97
|
+
"watch_db_pool",
|
|
98
|
+
"watch_thread_pool",
|
|
99
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
|
|
8
|
+
import anyio.to_thread
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.testclient import TestClient
|
|
11
|
+
import httpx2
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from fastapiobserver import ObservabilitySettings, install_observability
|
|
15
|
+
from fastapiobserver import pools
|
|
16
|
+
|
|
17
|
+
prometheus_client = pytest.importorskip("prometheus_client")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(autouse=True)
|
|
21
|
+
def _fresh_warnings() -> None:
|
|
22
|
+
pools._last_warned.clear()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _warnings(caplog: pytest.LogCaptureFixture, message: str) -> list[logging.LogRecord]:
|
|
26
|
+
return [r for r in caplog.records if r.getMessage() == message]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_requests_waiting_for_a_thread_are_reported(caplog: pytest.LogCaptureFixture) -> None:
|
|
30
|
+
app = FastAPI()
|
|
31
|
+
release = threading.Event()
|
|
32
|
+
|
|
33
|
+
@app.get("/sync")
|
|
34
|
+
def sync_endpoint() -> dict[str, str]: # runs in AnyIO's thread pool
|
|
35
|
+
release.wait(5)
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
@app.get("/ping")
|
|
39
|
+
async def ping() -> dict[str, str]:
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
install_observability(app, ObservabilitySettings(), metrics_enabled=True)
|
|
43
|
+
|
|
44
|
+
async def main() -> str:
|
|
45
|
+
async with app.router.lifespan_context(app):
|
|
46
|
+
anyio.to_thread.current_default_thread_limiter().total_tokens = 1
|
|
47
|
+
transport = httpx2.ASGITransport(app=app)
|
|
48
|
+
async with httpx2.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
49
|
+
blocked = [asyncio.create_task(client.get("/sync")) for _ in range(2)]
|
|
50
|
+
await asyncio.sleep(0.1) # one call holds the only thread, one waits
|
|
51
|
+
await client.get("/ping") # starts while the pool is full
|
|
52
|
+
await client.get("/ping") # within a minute: no second warning
|
|
53
|
+
metrics = prometheus_client.generate_latest().decode()
|
|
54
|
+
release.set()
|
|
55
|
+
await asyncio.gather(*blocked)
|
|
56
|
+
return metrics
|
|
57
|
+
|
|
58
|
+
with caplog.at_level(logging.WARNING, logger="fastapiobserver.pools"):
|
|
59
|
+
metrics = asyncio.run(main())
|
|
60
|
+
|
|
61
|
+
[warning] = _warnings(caplog, "threadpool.saturated")
|
|
62
|
+
assert warning.event == {"busy": 1, "limit": 1, "waiting": 1} # type: ignore[attr-defined]
|
|
63
|
+
assert "fastapiobserver_threadpool_busy 1.0" in metrics
|
|
64
|
+
assert "fastapiobserver_threadpool_limit 1.0" in metrics
|
|
65
|
+
assert "fastapiobserver_threadpool_waiting 1.0" in metrics
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _QueuePool:
|
|
69
|
+
"""The part of SQLAlchemy's QueuePool that fastapi-observer reads."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, size: int, max_overflow: int) -> None:
|
|
72
|
+
self._size = size
|
|
73
|
+
self._max_overflow = max_overflow
|
|
74
|
+
self.checked_out = 0
|
|
75
|
+
|
|
76
|
+
def size(self) -> int:
|
|
77
|
+
return self._size
|
|
78
|
+
|
|
79
|
+
def checkedout(self) -> int:
|
|
80
|
+
return self.checked_out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _Engine:
|
|
84
|
+
"""A SQLAlchemy engine: weakly referenceable, with a pool and a URL."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, pool: _QueuePool) -> None:
|
|
87
|
+
self.pool = pool
|
|
88
|
+
self.url = SimpleNamespace(render_as_string=lambda hide_password: "postgresql://app@db/orders")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_a_full_database_pool_is_reported(caplog: pytest.LogCaptureFixture) -> None:
|
|
92
|
+
pool = _QueuePool(size=2, max_overflow=1)
|
|
93
|
+
engine = _Engine(pool)
|
|
94
|
+
app = FastAPI()
|
|
95
|
+
|
|
96
|
+
@app.get("/ping")
|
|
97
|
+
async def ping() -> dict[str, str]:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
install_observability(app, ObservabilitySettings(), metrics_enabled=True, db_engine=engine)
|
|
101
|
+
client = TestClient(app)
|
|
102
|
+
with caplog.at_level(logging.WARNING, logger="fastapiobserver.pools"):
|
|
103
|
+
client.get("/ping") # 0 of 3 checked out
|
|
104
|
+
pool.checked_out = 3
|
|
105
|
+
client.get("/ping")
|
|
106
|
+
client.get("/ping") # within a minute: no second warning
|
|
107
|
+
|
|
108
|
+
[warning] = _warnings(caplog, "db.pool_exhausted")
|
|
109
|
+
assert warning.event == { # type: ignore[attr-defined]
|
|
110
|
+
"engine": "postgresql://app@db/orders",
|
|
111
|
+
"checked_out": 3,
|
|
112
|
+
"limit": 3,
|
|
113
|
+
}
|
|
114
|
+
metrics = prometheus_client.generate_latest().decode()
|
|
115
|
+
assert 'fastapiobserver_db_pool_checked_out{engine="postgresql://app@db/orders"} 3.0' in metrics
|
|
116
|
+
assert 'fastapiobserver_db_pool_limit{engine="postgresql://app@db/orders"} 3.0' in metrics
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.12.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapi_observer.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.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.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/integrations/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/integrations/strawberry.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/logging/circuit_breaker.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.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.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/body_capture.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/context.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/events.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/middleware/headers.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/security/normalize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.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.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/dlq.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/src/fastapiobserver/sinks/logtail/handler.py
RENAMED
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.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
|
|
File without changes
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.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
|
{fastapi_observer-1.12.0 → fastapi_observer-1.13.0}/tests/test_route_template_cardinality.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
|