fastapi-observer 0.3.2__tar.gz → 0.4.1__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-0.3.2/src/fastapi_observer.egg-info → fastapi_observer-0.4.1}/PKG-INFO +22 -5
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/README.md +21 -4
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1/src/fastapi_observer.egg-info}/PKG-INFO +22 -5
- fastapi_observer-0.4.1/src/fastapi_observer.egg-info/SOURCES.txt +106 -0
- fastapi_observer-0.4.1/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/control_plane.py +7 -1
- fastapi_observer-0.4.1/src/fastapiobserver/logging/__init__.py +36 -0
- fastapi_observer-0.4.1/src/fastapiobserver/logging/circuit_breaker.py +158 -0
- fastapi_observer-0.4.1/src/fastapiobserver/logging/filters.py +53 -0
- fastapi_observer-0.4.1/src/fastapiobserver/logging/formatter.py +157 -0
- fastapi_observer-0.4.1/src/fastapiobserver/logging/queueing.py +197 -0
- fastapi_observer-0.4.1/src/fastapiobserver/logging/setup.py +239 -0
- fastapi_observer-0.4.1/src/fastapiobserver/logging/state.py +17 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/__init__.py +41 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/builder.py +48 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/contracts.py +39 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/endpoint.py +176 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/noop.py +15 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/pathing.py +18 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/prometheus/__init__.py +21 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/prometheus/backend.py +126 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/prometheus/client.py +19 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/prometheus/collector.py +170 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/prometheus/exemplars.py +19 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/prometheus/multiprocess.py +38 -0
- fastapi_observer-0.4.1/src/fastapiobserver/metrics/registry.py +42 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/__init__.py +27 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/body_capture.py +37 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/context.py +63 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/events.py +90 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/headers.py +21 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/ip.py +57 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/metrics.py +61 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/request_logging.py +154 -0
- fastapi_observer-0.4.1/src/fastapiobserver/middleware/span_errors.py +34 -0
- fastapi_observer-0.4.1/src/fastapiobserver/security/__init__.py +31 -0
- fastapi_observer-0.4.1/src/fastapiobserver/security/normalize.py +8 -0
- fastapi_observer-0.3.2/src/fastapiobserver/security.py → fastapi_observer-0.4.1/src/fastapiobserver/security/policy.py +20 -203
- fastapi_observer-0.4.1/src/fastapiobserver/security/proxies.py +49 -0
- fastapi_observer-0.4.1/src/fastapiobserver/security/redaction.py +154 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/__init__.py +25 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/builtin.py +56 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/discovery.py +68 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/factory.py +73 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/logtail/__init__.py +5 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/logtail/dlq.py +130 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/logtail/handler.py +205 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/logtail/sink.py +66 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/protocol.py +15 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/registry.py +33 -0
- fastapi_observer-0.4.1/src/fastapiobserver/sinks/stats.py +55 -0
- fastapi_observer-0.4.1/tests/test_dx_parity.py +79 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_log_queue_controls.py +17 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_logging_schema_contract.py +33 -12
- fastapi_observer-0.4.1/tests/test_metrics_contracts.py +111 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_metrics_optional_dependency.py +4 -4
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_middleware.py +34 -0
- fastapi_observer-0.4.1/tests/test_security_audit_fixes.py +161 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_sink_circuit_breaker.py +21 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_sinks.py +80 -36
- fastapi_observer-0.3.2/src/fastapi_observer.egg-info/SOURCES.txt +0 -63
- fastapi_observer-0.3.2/src/fastapiobserver/_version.py +0 -1
- fastapi_observer-0.3.2/src/fastapiobserver/logging.py +0 -825
- fastapi_observer-0.3.2/src/fastapiobserver/metrics.py +0 -668
- fastapi_observer-0.3.2/src/fastapiobserver/middleware.py +0 -447
- fastapi_observer-0.3.2/src/fastapiobserver/sinks.py +0 -677
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/LICENSE +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/pyproject.toml +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/setup.cfg +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapi_observer.egg-info/requires.txt +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/fastapi.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/__init__.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/lifecycle.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/logs.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/metrics.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/resource.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/tracing.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/otel/types.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/request_context.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_config_settings.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_excluded_url_precedence.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_negotiate_content_type.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otel_integration.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otel_lifecycle.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otel_log_correlation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otel_logs_pipeline.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_otlp_export_integration.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_propagation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_route_template_cardinality.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_trust_boundary.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.1}/tests/test_utils_refactors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-observer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -392,6 +392,19 @@ Request arrives
|
|
|
392
392
|
-> context cleared
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
+
### Internal Package Layout (Contributor Map)
|
|
396
|
+
|
|
397
|
+
The project is now organized as focused subpackages instead of large monolithic modules:
|
|
398
|
+
|
|
399
|
+
- `fastapiobserver/logging/`: formatter, queueing, filters, setup lifecycle, sink circuit-breakers.
|
|
400
|
+
- `fastapiobserver/middleware/`: request logging orchestration, context, IP resolution, headers, body capture, metrics hooks.
|
|
401
|
+
- `fastapiobserver/sinks/`: sink protocol, registry/discovery, built-ins, factory wiring, Logtail + DLQ implementation.
|
|
402
|
+
- `fastapiobserver/metrics/`: backend contracts/registry/builder/endpoint, Prometheus integration subpackage.
|
|
403
|
+
- `fastapiobserver/security/`: policy/settings models, normalization helpers, redaction engine, trusted-proxy utilities.
|
|
404
|
+
- `fastapiobserver/otel/`: OTel settings/resource/tracing/logs/metrics/lifecycle helpers.
|
|
405
|
+
|
|
406
|
+
Public imports remain backward-compatible via package facades (`__init__.py` re-exports).
|
|
407
|
+
|
|
395
408
|
---
|
|
396
409
|
|
|
397
410
|
## Example JSON Log Event
|
|
@@ -691,6 +704,9 @@ The library supports configuration from code and env vars. Below are the most re
|
|
|
691
704
|
| `METRICS_EXEMPLARS_ENABLED` | `false` | Enable exemplars where supported |
|
|
692
705
|
| `METRICS_FORMAT` | `negotiate` | `prometheus`, `openmetrics`, or `negotiate` |
|
|
693
706
|
|
|
707
|
+
> [!CAUTION]
|
|
708
|
+
> The `/metrics` endpoint is **unauthenticated by default**. In production it should be restricted to internal networks (e.g. behind a Kubernetes `NetworkPolicy`, VPC security group, or ingress rule that only allows your Prometheus scraper). Exposing it publicly leaks service topology, error rates, and request patterns.
|
|
709
|
+
|
|
694
710
|
### Security and trust boundary
|
|
695
711
|
|
|
696
712
|
| Variable | Default | Description |
|
|
@@ -1028,8 +1044,8 @@ Repository integration tests include:
|
|
|
1028
1044
|
|
|
1029
1045
|
Reproducible benchmark harness and methodology:
|
|
1030
1046
|
- Guide: [`benchmarks.md`](benchmarks.md)
|
|
1031
|
-
- Apps: `examples/benchmarks/
|
|
1032
|
-
- Runner: `examples/benchmarks/
|
|
1047
|
+
- Apps: `examples/benchmarks/app.py`
|
|
1048
|
+
- Runner: `examples/benchmarks/harness.py`
|
|
1033
1049
|
|
|
1034
1050
|
---
|
|
1035
1051
|
|
|
@@ -1037,9 +1053,10 @@ Reproducible benchmark harness and methodology:
|
|
|
1037
1053
|
|
|
1038
1054
|
- `0.1.x`: secure-by-default core
|
|
1039
1055
|
- `0.2.x`: OTel interoperability, security presets, allowlists
|
|
1056
|
+
- `0.3.x`: package modularization, sink/metrics hardening, and performance reliability updates
|
|
1040
1057
|
- `1.0.0`: dynamic runtime controls and plugin stability
|
|
1041
1058
|
|
|
1042
|
-
Current release version: `0.
|
|
1059
|
+
Current release version: `0.4.1`
|
|
1043
1060
|
|
|
1044
1061
|
## Changelog Policy
|
|
1045
1062
|
|
|
@@ -1094,4 +1111,4 @@ The pre-push hook runs:
|
|
|
1094
1111
|
|
|
1095
1112
|
## Roadmap Tracking
|
|
1096
1113
|
|
|
1097
|
-
See [NEXT_STEPS.md](NEXT_STEPS.md) for the active
|
|
1114
|
+
See [NEXT_STEPS.md](NEXT_STEPS.md) for the active roadmap and release checklist.
|
|
@@ -322,6 +322,19 @@ Request arrives
|
|
|
322
322
|
-> context cleared
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
+
### Internal Package Layout (Contributor Map)
|
|
326
|
+
|
|
327
|
+
The project is now organized as focused subpackages instead of large monolithic modules:
|
|
328
|
+
|
|
329
|
+
- `fastapiobserver/logging/`: formatter, queueing, filters, setup lifecycle, sink circuit-breakers.
|
|
330
|
+
- `fastapiobserver/middleware/`: request logging orchestration, context, IP resolution, headers, body capture, metrics hooks.
|
|
331
|
+
- `fastapiobserver/sinks/`: sink protocol, registry/discovery, built-ins, factory wiring, Logtail + DLQ implementation.
|
|
332
|
+
- `fastapiobserver/metrics/`: backend contracts/registry/builder/endpoint, Prometheus integration subpackage.
|
|
333
|
+
- `fastapiobserver/security/`: policy/settings models, normalization helpers, redaction engine, trusted-proxy utilities.
|
|
334
|
+
- `fastapiobserver/otel/`: OTel settings/resource/tracing/logs/metrics/lifecycle helpers.
|
|
335
|
+
|
|
336
|
+
Public imports remain backward-compatible via package facades (`__init__.py` re-exports).
|
|
337
|
+
|
|
325
338
|
---
|
|
326
339
|
|
|
327
340
|
## Example JSON Log Event
|
|
@@ -621,6 +634,9 @@ The library supports configuration from code and env vars. Below are the most re
|
|
|
621
634
|
| `METRICS_EXEMPLARS_ENABLED` | `false` | Enable exemplars where supported |
|
|
622
635
|
| `METRICS_FORMAT` | `negotiate` | `prometheus`, `openmetrics`, or `negotiate` |
|
|
623
636
|
|
|
637
|
+
> [!CAUTION]
|
|
638
|
+
> The `/metrics` endpoint is **unauthenticated by default**. In production it should be restricted to internal networks (e.g. behind a Kubernetes `NetworkPolicy`, VPC security group, or ingress rule that only allows your Prometheus scraper). Exposing it publicly leaks service topology, error rates, and request patterns.
|
|
639
|
+
|
|
624
640
|
### Security and trust boundary
|
|
625
641
|
|
|
626
642
|
| Variable | Default | Description |
|
|
@@ -958,8 +974,8 @@ Repository integration tests include:
|
|
|
958
974
|
|
|
959
975
|
Reproducible benchmark harness and methodology:
|
|
960
976
|
- Guide: [`benchmarks.md`](benchmarks.md)
|
|
961
|
-
- Apps: `examples/benchmarks/
|
|
962
|
-
- Runner: `examples/benchmarks/
|
|
977
|
+
- Apps: `examples/benchmarks/app.py`
|
|
978
|
+
- Runner: `examples/benchmarks/harness.py`
|
|
963
979
|
|
|
964
980
|
---
|
|
965
981
|
|
|
@@ -967,9 +983,10 @@ Reproducible benchmark harness and methodology:
|
|
|
967
983
|
|
|
968
984
|
- `0.1.x`: secure-by-default core
|
|
969
985
|
- `0.2.x`: OTel interoperability, security presets, allowlists
|
|
986
|
+
- `0.3.x`: package modularization, sink/metrics hardening, and performance reliability updates
|
|
970
987
|
- `1.0.0`: dynamic runtime controls and plugin stability
|
|
971
988
|
|
|
972
|
-
Current release version: `0.
|
|
989
|
+
Current release version: `0.4.1`
|
|
973
990
|
|
|
974
991
|
## Changelog Policy
|
|
975
992
|
|
|
@@ -1024,4 +1041,4 @@ The pre-push hook runs:
|
|
|
1024
1041
|
|
|
1025
1042
|
## Roadmap Tracking
|
|
1026
1043
|
|
|
1027
|
-
See [NEXT_STEPS.md](NEXT_STEPS.md) for the active
|
|
1044
|
+
See [NEXT_STEPS.md](NEXT_STEPS.md) for the active roadmap and release checklist.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-observer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -392,6 +392,19 @@ Request arrives
|
|
|
392
392
|
-> context cleared
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
+
### Internal Package Layout (Contributor Map)
|
|
396
|
+
|
|
397
|
+
The project is now organized as focused subpackages instead of large monolithic modules:
|
|
398
|
+
|
|
399
|
+
- `fastapiobserver/logging/`: formatter, queueing, filters, setup lifecycle, sink circuit-breakers.
|
|
400
|
+
- `fastapiobserver/middleware/`: request logging orchestration, context, IP resolution, headers, body capture, metrics hooks.
|
|
401
|
+
- `fastapiobserver/sinks/`: sink protocol, registry/discovery, built-ins, factory wiring, Logtail + DLQ implementation.
|
|
402
|
+
- `fastapiobserver/metrics/`: backend contracts/registry/builder/endpoint, Prometheus integration subpackage.
|
|
403
|
+
- `fastapiobserver/security/`: policy/settings models, normalization helpers, redaction engine, trusted-proxy utilities.
|
|
404
|
+
- `fastapiobserver/otel/`: OTel settings/resource/tracing/logs/metrics/lifecycle helpers.
|
|
405
|
+
|
|
406
|
+
Public imports remain backward-compatible via package facades (`__init__.py` re-exports).
|
|
407
|
+
|
|
395
408
|
---
|
|
396
409
|
|
|
397
410
|
## Example JSON Log Event
|
|
@@ -691,6 +704,9 @@ The library supports configuration from code and env vars. Below are the most re
|
|
|
691
704
|
| `METRICS_EXEMPLARS_ENABLED` | `false` | Enable exemplars where supported |
|
|
692
705
|
| `METRICS_FORMAT` | `negotiate` | `prometheus`, `openmetrics`, or `negotiate` |
|
|
693
706
|
|
|
707
|
+
> [!CAUTION]
|
|
708
|
+
> The `/metrics` endpoint is **unauthenticated by default**. In production it should be restricted to internal networks (e.g. behind a Kubernetes `NetworkPolicy`, VPC security group, or ingress rule that only allows your Prometheus scraper). Exposing it publicly leaks service topology, error rates, and request patterns.
|
|
709
|
+
|
|
694
710
|
### Security and trust boundary
|
|
695
711
|
|
|
696
712
|
| Variable | Default | Description |
|
|
@@ -1028,8 +1044,8 @@ Repository integration tests include:
|
|
|
1028
1044
|
|
|
1029
1045
|
Reproducible benchmark harness and methodology:
|
|
1030
1046
|
- Guide: [`benchmarks.md`](benchmarks.md)
|
|
1031
|
-
- Apps: `examples/benchmarks/
|
|
1032
|
-
- Runner: `examples/benchmarks/
|
|
1047
|
+
- Apps: `examples/benchmarks/app.py`
|
|
1048
|
+
- Runner: `examples/benchmarks/harness.py`
|
|
1033
1049
|
|
|
1034
1050
|
---
|
|
1035
1051
|
|
|
@@ -1037,9 +1053,10 @@ Reproducible benchmark harness and methodology:
|
|
|
1037
1053
|
|
|
1038
1054
|
- `0.1.x`: secure-by-default core
|
|
1039
1055
|
- `0.2.x`: OTel interoperability, security presets, allowlists
|
|
1056
|
+
- `0.3.x`: package modularization, sink/metrics hardening, and performance reliability updates
|
|
1040
1057
|
- `1.0.0`: dynamic runtime controls and plugin stability
|
|
1041
1058
|
|
|
1042
|
-
Current release version: `0.
|
|
1059
|
+
Current release version: `0.4.1`
|
|
1043
1060
|
|
|
1044
1061
|
## Changelog Policy
|
|
1045
1062
|
|
|
@@ -1094,4 +1111,4 @@ The pre-push hook runs:
|
|
|
1094
1111
|
|
|
1095
1112
|
## Roadmap Tracking
|
|
1096
1113
|
|
|
1097
|
-
See [NEXT_STEPS.md](NEXT_STEPS.md) for the active
|
|
1114
|
+
See [NEXT_STEPS.md](NEXT_STEPS.md) for the active roadmap and release checklist.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/fastapi_observer.egg-info/PKG-INFO
|
|
5
|
+
src/fastapi_observer.egg-info/SOURCES.txt
|
|
6
|
+
src/fastapi_observer.egg-info/dependency_links.txt
|
|
7
|
+
src/fastapi_observer.egg-info/requires.txt
|
|
8
|
+
src/fastapi_observer.egg-info/top_level.txt
|
|
9
|
+
src/fastapiobserver/__init__.py
|
|
10
|
+
src/fastapiobserver/_version.py
|
|
11
|
+
src/fastapiobserver/config.py
|
|
12
|
+
src/fastapiobserver/control_plane.py
|
|
13
|
+
src/fastapiobserver/fastapi.py
|
|
14
|
+
src/fastapiobserver/loguru.py
|
|
15
|
+
src/fastapiobserver/plugins.py
|
|
16
|
+
src/fastapiobserver/propagation.py
|
|
17
|
+
src/fastapiobserver/py.typed
|
|
18
|
+
src/fastapiobserver/request_context.py
|
|
19
|
+
src/fastapiobserver/utils.py
|
|
20
|
+
src/fastapiobserver/integrations/__init__.py
|
|
21
|
+
src/fastapiobserver/integrations/strawberry.py
|
|
22
|
+
src/fastapiobserver/logging/__init__.py
|
|
23
|
+
src/fastapiobserver/logging/circuit_breaker.py
|
|
24
|
+
src/fastapiobserver/logging/filters.py
|
|
25
|
+
src/fastapiobserver/logging/formatter.py
|
|
26
|
+
src/fastapiobserver/logging/queueing.py
|
|
27
|
+
src/fastapiobserver/logging/setup.py
|
|
28
|
+
src/fastapiobserver/logging/state.py
|
|
29
|
+
src/fastapiobserver/metrics/__init__.py
|
|
30
|
+
src/fastapiobserver/metrics/builder.py
|
|
31
|
+
src/fastapiobserver/metrics/contracts.py
|
|
32
|
+
src/fastapiobserver/metrics/endpoint.py
|
|
33
|
+
src/fastapiobserver/metrics/noop.py
|
|
34
|
+
src/fastapiobserver/metrics/pathing.py
|
|
35
|
+
src/fastapiobserver/metrics/registry.py
|
|
36
|
+
src/fastapiobserver/metrics/prometheus/__init__.py
|
|
37
|
+
src/fastapiobserver/metrics/prometheus/backend.py
|
|
38
|
+
src/fastapiobserver/metrics/prometheus/client.py
|
|
39
|
+
src/fastapiobserver/metrics/prometheus/collector.py
|
|
40
|
+
src/fastapiobserver/metrics/prometheus/exemplars.py
|
|
41
|
+
src/fastapiobserver/metrics/prometheus/multiprocess.py
|
|
42
|
+
src/fastapiobserver/middleware/__init__.py
|
|
43
|
+
src/fastapiobserver/middleware/body_capture.py
|
|
44
|
+
src/fastapiobserver/middleware/context.py
|
|
45
|
+
src/fastapiobserver/middleware/events.py
|
|
46
|
+
src/fastapiobserver/middleware/headers.py
|
|
47
|
+
src/fastapiobserver/middleware/ip.py
|
|
48
|
+
src/fastapiobserver/middleware/metrics.py
|
|
49
|
+
src/fastapiobserver/middleware/request_logging.py
|
|
50
|
+
src/fastapiobserver/middleware/span_errors.py
|
|
51
|
+
src/fastapiobserver/otel/__init__.py
|
|
52
|
+
src/fastapiobserver/otel/lifecycle.py
|
|
53
|
+
src/fastapiobserver/otel/logs.py
|
|
54
|
+
src/fastapiobserver/otel/metrics.py
|
|
55
|
+
src/fastapiobserver/otel/resource.py
|
|
56
|
+
src/fastapiobserver/otel/settings.py
|
|
57
|
+
src/fastapiobserver/otel/tracing.py
|
|
58
|
+
src/fastapiobserver/otel/types.py
|
|
59
|
+
src/fastapiobserver/security/__init__.py
|
|
60
|
+
src/fastapiobserver/security/normalize.py
|
|
61
|
+
src/fastapiobserver/security/policy.py
|
|
62
|
+
src/fastapiobserver/security/proxies.py
|
|
63
|
+
src/fastapiobserver/security/redaction.py
|
|
64
|
+
src/fastapiobserver/sinks/__init__.py
|
|
65
|
+
src/fastapiobserver/sinks/builtin.py
|
|
66
|
+
src/fastapiobserver/sinks/discovery.py
|
|
67
|
+
src/fastapiobserver/sinks/factory.py
|
|
68
|
+
src/fastapiobserver/sinks/protocol.py
|
|
69
|
+
src/fastapiobserver/sinks/registry.py
|
|
70
|
+
src/fastapiobserver/sinks/stats.py
|
|
71
|
+
src/fastapiobserver/sinks/logtail/__init__.py
|
|
72
|
+
src/fastapiobserver/sinks/logtail/dlq.py
|
|
73
|
+
src/fastapiobserver/sinks/logtail/handler.py
|
|
74
|
+
src/fastapiobserver/sinks/logtail/sink.py
|
|
75
|
+
tests/test_body_media_allowlist.py
|
|
76
|
+
tests/test_config_settings.py
|
|
77
|
+
tests/test_dx_parity.py
|
|
78
|
+
tests/test_excluded_url_precedence.py
|
|
79
|
+
tests/test_fastapi_install.py
|
|
80
|
+
tests/test_graphql_integrations.py
|
|
81
|
+
tests/test_log_queue_controls.py
|
|
82
|
+
tests/test_logging_schema_contract.py
|
|
83
|
+
tests/test_logtail_dlq.py
|
|
84
|
+
tests/test_loguru_bridge.py
|
|
85
|
+
tests/test_metrics_contracts.py
|
|
86
|
+
tests/test_metrics_optional_dependency.py
|
|
87
|
+
tests/test_middleware.py
|
|
88
|
+
tests/test_negotiate_content_type.py
|
|
89
|
+
tests/test_otel_integration.py
|
|
90
|
+
tests/test_otel_lifecycle.py
|
|
91
|
+
tests/test_otel_log_correlation.py
|
|
92
|
+
tests/test_otel_logs_from_env.py
|
|
93
|
+
tests/test_otel_logs_pipeline.py
|
|
94
|
+
tests/test_otel_metrics_from_env.py
|
|
95
|
+
tests/test_otlp_export_integration.py
|
|
96
|
+
tests/test_plugin_isolation.py
|
|
97
|
+
tests/test_propagation.py
|
|
98
|
+
tests/test_redaction_presets.py
|
|
99
|
+
tests/test_route_template_cardinality.py
|
|
100
|
+
tests/test_runtime_control_auth.py
|
|
101
|
+
tests/test_security_audit_fixes.py
|
|
102
|
+
tests/test_security_redaction.py
|
|
103
|
+
tests/test_sink_circuit_breaker.py
|
|
104
|
+
tests/test_sinks.py
|
|
105
|
+
tests/test_trust_boundary.py
|
|
106
|
+
tests/test_utils_refactors.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1"
|
|
@@ -72,7 +72,13 @@ def mount_control_plane(app: FastAPI, settings: RuntimeControlSettings) -> None:
|
|
|
72
72
|
def authorize(
|
|
73
73
|
authorization: Annotated[str | None, Header()] = None,
|
|
74
74
|
) -> None:
|
|
75
|
-
|
|
75
|
+
current_token = os.getenv(settings.token_env_var)
|
|
76
|
+
if not current_token:
|
|
77
|
+
raise HTTPException(
|
|
78
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
79
|
+
detail="Unauthorized",
|
|
80
|
+
)
|
|
81
|
+
expected = f"Bearer {current_token}"
|
|
76
82
|
if not authorization or not secrets.compare_digest(authorization, expected):
|
|
77
83
|
raise HTTPException(
|
|
78
84
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .circuit_breaker import (
|
|
4
|
+
CircuitBreakerState,
|
|
5
|
+
SinkCircuitBreakerHandler,
|
|
6
|
+
SinkCircuitBreakerSnapshot,
|
|
7
|
+
get_sink_circuit_breaker_stats,
|
|
8
|
+
)
|
|
9
|
+
from .filters import PluginLogFilter, RequestIdFilter, TraceContextFilter
|
|
10
|
+
from .formatter import LOG_SCHEMA_VERSION, StructuredJsonFormatter
|
|
11
|
+
from .queueing import (
|
|
12
|
+
LogQueueStatsSnapshot,
|
|
13
|
+
LogQueueTelemetry,
|
|
14
|
+
OverflowPolicyQueueHandler,
|
|
15
|
+
get_log_queue_stats,
|
|
16
|
+
)
|
|
17
|
+
from .setup import get_managed_output_handlers, setup_logging, shutdown_logging
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"LOG_SCHEMA_VERSION",
|
|
21
|
+
"CircuitBreakerState",
|
|
22
|
+
"LogQueueStatsSnapshot",
|
|
23
|
+
"LogQueueTelemetry",
|
|
24
|
+
"OverflowPolicyQueueHandler",
|
|
25
|
+
"PluginLogFilter",
|
|
26
|
+
"RequestIdFilter",
|
|
27
|
+
"SinkCircuitBreakerHandler",
|
|
28
|
+
"SinkCircuitBreakerSnapshot",
|
|
29
|
+
"StructuredJsonFormatter",
|
|
30
|
+
"TraceContextFilter",
|
|
31
|
+
"get_log_queue_stats",
|
|
32
|
+
"get_managed_output_handlers",
|
|
33
|
+
"get_sink_circuit_breaker_stats",
|
|
34
|
+
"setup_logging",
|
|
35
|
+
"shutdown_logging",
|
|
36
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable, Literal
|
|
8
|
+
|
|
9
|
+
CircuitBreakerState = Literal["closed", "open", "half_open"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SinkCircuitBreakerSnapshot:
|
|
14
|
+
sink_name: str
|
|
15
|
+
state: CircuitBreakerState
|
|
16
|
+
failure_threshold: int
|
|
17
|
+
recovery_timeout_seconds: float
|
|
18
|
+
consecutive_failures: int
|
|
19
|
+
handled_total: int
|
|
20
|
+
failures_total: int
|
|
21
|
+
skipped_total: int
|
|
22
|
+
opens_total: int
|
|
23
|
+
half_open_total: int
|
|
24
|
+
closes_total: int
|
|
25
|
+
|
|
26
|
+
def as_dict(self) -> dict[str, int | float | str]:
|
|
27
|
+
return {
|
|
28
|
+
"sink_name": self.sink_name,
|
|
29
|
+
"state": self.state,
|
|
30
|
+
"failure_threshold": self.failure_threshold,
|
|
31
|
+
"recovery_timeout_seconds": self.recovery_timeout_seconds,
|
|
32
|
+
"consecutive_failures": self.consecutive_failures,
|
|
33
|
+
"handled_total": self.handled_total,
|
|
34
|
+
"failures_total": self.failures_total,
|
|
35
|
+
"skipped_total": self.skipped_total,
|
|
36
|
+
"opens_total": self.opens_total,
|
|
37
|
+
"half_open_total": self.half_open_total,
|
|
38
|
+
"closes_total": self.closes_total,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SinkCircuitBreakerHandler(logging.Handler):
|
|
43
|
+
"""Protect sink handlers with a basic open/half-open/closed breaker."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
sink_name: str,
|
|
49
|
+
delegate: logging.Handler,
|
|
50
|
+
failure_threshold: int,
|
|
51
|
+
recovery_timeout_seconds: float,
|
|
52
|
+
clock: Callable[[], float] | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(level=delegate.level)
|
|
55
|
+
self.sink_name = sink_name
|
|
56
|
+
self._delegate = delegate
|
|
57
|
+
self._failure_threshold = max(1, int(failure_threshold))
|
|
58
|
+
self._recovery_timeout_seconds = max(0.001, float(recovery_timeout_seconds))
|
|
59
|
+
self._clock = clock or time.monotonic
|
|
60
|
+
self._lock = threading.Lock()
|
|
61
|
+
|
|
62
|
+
self._state: CircuitBreakerState = "closed"
|
|
63
|
+
self._opened_until = 0.0
|
|
64
|
+
self._consecutive_failures = 0
|
|
65
|
+
self._handled_total = 0
|
|
66
|
+
self._failures_total = 0
|
|
67
|
+
self._skipped_total = 0
|
|
68
|
+
self._opens_total = 0
|
|
69
|
+
self._half_open_total = 0
|
|
70
|
+
self._closes_total = 0
|
|
71
|
+
|
|
72
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
73
|
+
if self._should_skip():
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
self._delegate.handle(record)
|
|
78
|
+
except Exception:
|
|
79
|
+
self._record_failure()
|
|
80
|
+
self.handleError(record)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
self._record_success()
|
|
84
|
+
|
|
85
|
+
def setFormatter(self, fmt: logging.Formatter | None) -> None: # noqa: N802
|
|
86
|
+
super().setFormatter(fmt)
|
|
87
|
+
self._delegate.setFormatter(fmt)
|
|
88
|
+
|
|
89
|
+
def flush(self) -> None:
|
|
90
|
+
self._delegate.flush()
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
try:
|
|
94
|
+
self._delegate.close()
|
|
95
|
+
finally:
|
|
96
|
+
super().close()
|
|
97
|
+
|
|
98
|
+
def snapshot(self) -> SinkCircuitBreakerSnapshot:
|
|
99
|
+
with self._lock:
|
|
100
|
+
return SinkCircuitBreakerSnapshot(
|
|
101
|
+
sink_name=self.sink_name,
|
|
102
|
+
state=self._state,
|
|
103
|
+
failure_threshold=self._failure_threshold,
|
|
104
|
+
recovery_timeout_seconds=self._recovery_timeout_seconds,
|
|
105
|
+
consecutive_failures=self._consecutive_failures,
|
|
106
|
+
handled_total=self._handled_total,
|
|
107
|
+
failures_total=self._failures_total,
|
|
108
|
+
skipped_total=self._skipped_total,
|
|
109
|
+
opens_total=self._opens_total,
|
|
110
|
+
half_open_total=self._half_open_total,
|
|
111
|
+
closes_total=self._closes_total,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _should_skip(self) -> bool:
|
|
115
|
+
with self._lock:
|
|
116
|
+
now = self._clock()
|
|
117
|
+
if self._state != "open":
|
|
118
|
+
return False
|
|
119
|
+
if now >= self._opened_until:
|
|
120
|
+
self._state = "half_open"
|
|
121
|
+
self._half_open_total += 1
|
|
122
|
+
return False
|
|
123
|
+
self._skipped_total += 1
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def _record_failure(self) -> None:
|
|
127
|
+
with self._lock:
|
|
128
|
+
now = self._clock()
|
|
129
|
+
self._failures_total += 1
|
|
130
|
+
self._consecutive_failures += 1
|
|
131
|
+
|
|
132
|
+
should_open = (
|
|
133
|
+
self._state == "half_open"
|
|
134
|
+
or self._consecutive_failures >= self._failure_threshold
|
|
135
|
+
)
|
|
136
|
+
if should_open:
|
|
137
|
+
self._state = "open"
|
|
138
|
+
self._opens_total += 1
|
|
139
|
+
self._opened_until = now + self._recovery_timeout_seconds
|
|
140
|
+
self._consecutive_failures = 0
|
|
141
|
+
|
|
142
|
+
def _record_success(self) -> None:
|
|
143
|
+
with self._lock:
|
|
144
|
+
self._handled_total += 1
|
|
145
|
+
self._consecutive_failures = 0
|
|
146
|
+
if self._state == "half_open":
|
|
147
|
+
self._state = "closed"
|
|
148
|
+
self._closes_total += 1
|
|
149
|
+
self._opened_until = 0.0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_sink_circuit_breaker_stats() -> dict[str, dict[str, int | float | str]]:
|
|
153
|
+
"""Return per-sink circuit-breaker snapshots."""
|
|
154
|
+
from .state import _LOGGING_LOCK, _SINK_CIRCUIT_BREAKERS
|
|
155
|
+
|
|
156
|
+
with _LOGGING_LOCK:
|
|
157
|
+
breakers = list(_SINK_CIRCUIT_BREAKERS)
|
|
158
|
+
return {breaker.sink_name: breaker.snapshot().as_dict() for breaker in breakers}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from ..plugins import apply_log_filters
|
|
6
|
+
from ..request_context import get_request_id, get_span_id, get_trace_id
|
|
7
|
+
|
|
8
|
+
_LOGGER = logging.getLogger("fastapiobserver.logging")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RequestIdFilter(logging.Filter):
|
|
12
|
+
"""Attach request ID from context vars to every log record."""
|
|
13
|
+
|
|
14
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
15
|
+
if not getattr(record, "request_id", None):
|
|
16
|
+
record.request_id = get_request_id()
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TraceContextFilter(logging.Filter):
|
|
21
|
+
"""Attach OTel trace_id / span_id to every log record."""
|
|
22
|
+
|
|
23
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
24
|
+
trace_id = get_trace_id()
|
|
25
|
+
span_id = get_span_id()
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from opentelemetry import trace as otel_trace # type: ignore
|
|
29
|
+
|
|
30
|
+
span = otel_trace.get_current_span()
|
|
31
|
+
span_context = span.get_span_context()
|
|
32
|
+
if span_context and span_context.is_valid:
|
|
33
|
+
trace_id = f"{span_context.trace_id:032x}"
|
|
34
|
+
span_id = f"{span_context.span_id:016x}"
|
|
35
|
+
except Exception:
|
|
36
|
+
_LOGGER.debug(
|
|
37
|
+
"trace_context_filter.otel_lookup_failed",
|
|
38
|
+
exc_info=True,
|
|
39
|
+
extra={"_skip_enrichers": True},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if trace_id:
|
|
43
|
+
record.trace_id = trace_id # type: ignore[attr-defined]
|
|
44
|
+
if span_id:
|
|
45
|
+
record.span_id = span_id # type: ignore[attr-defined]
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PluginLogFilter(logging.Filter):
|
|
50
|
+
"""Apply user-registered log filters with fault isolation."""
|
|
51
|
+
|
|
52
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
53
|
+
return apply_log_filters(record)
|