fastapi-observer 0.3.1__tar.gz → 0.3.2__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.1/src/fastapi_observer.egg-info → fastapi_observer-0.3.2}/PKG-INFO +79 -3
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/README.md +77 -2
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/pyproject.toml +1 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2/src/fastapi_observer.egg-info}/PKG-INFO +79 -3
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapi_observer.egg-info/requires.txt +1 -0
- fastapi_observer-0.3.2/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/middleware.py +23 -19
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/security.py +10 -2
- fastapi_observer-0.3.1/src/fastapiobserver/_version.py +0 -1
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/LICENSE +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/setup.cfg +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapi_observer.egg-info/SOURCES.txt +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/control_plane.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/fastapi.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/logging.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/metrics.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/__init__.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/lifecycle.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/logs.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/metrics.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/resource.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/tracing.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/otel/types.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/request_context.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/sinks.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_config_settings.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_excluded_url_precedence.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_log_queue_controls.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_logging_schema_contract.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_metrics_optional_dependency.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_middleware.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_negotiate_content_type.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otel_integration.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otel_lifecycle.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otel_log_correlation.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otel_logs_pipeline.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_otlp_export_integration.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_propagation.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_route_template_cardinality.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_sink_circuit_breaker.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_sinks.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/tests/test_trust_boundary.py +0 -0
- {fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/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.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -65,6 +65,7 @@ Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
|
65
65
|
Requires-Dist: pytest-randomly>=3.16.0; extra == "dev"
|
|
66
66
|
Requires-Dist: ruff>=0.15.1; extra == "dev"
|
|
67
67
|
Requires-Dist: cyclonedx-bom>=7.2.1; extra == "dev"
|
|
68
|
+
Requires-Dist: uvicorn>=0.30.0; extra == "dev"
|
|
68
69
|
Dynamic: license-file
|
|
69
70
|
|
|
70
71
|
# fastapi-observer
|
|
@@ -570,11 +571,57 @@ kubectl -n observability rollout status deployment/traffic-generator
|
|
|
570
571
|
kubectl -n observability port-forward svc/grafana 3000:3000
|
|
571
572
|
```
|
|
572
573
|
|
|
573
|
-
Open [http://localhost:3000](http://localhost:3000).
|
|
574
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
574
575
|
Full guide: [`kubernetes.md`](kubernetes.md)
|
|
575
576
|
|
|
576
577
|
---
|
|
577
578
|
|
|
579
|
+
## Low-Overhead & Production Tuning (Advanced)
|
|
580
|
+
|
|
581
|
+
`fastapi-observer` integrates natively with the core OpenTelemetry Python SDK, meaning you can aggressively tune its resource usage purely via standard environment variables without altering your application code.
|
|
582
|
+
|
|
583
|
+
For high-throughput services (e.g. `10k+ RPS`), apply these exact variables to minimize the observer footprint:
|
|
584
|
+
|
|
585
|
+
### 1. Head-Based Sampling
|
|
586
|
+
|
|
587
|
+
Tracing 100% of requests is too expensive at scale. You should configure `fastapi-observer` to respect upstream trace flags, while only sampling a fraction of net-new requests:
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
# Keep the parent's sample decision if it exists, otherwise sample 5%
|
|
591
|
+
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
|
|
592
|
+
export OTEL_TRACES_SAMPLER_ARG="0.05"
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 2. Exclude Noisy URLs from the SDK
|
|
596
|
+
|
|
597
|
+
Do not waste cycles generating spans for health checks or static assets. `fastapi-observer` will auto-derive metrics exclusions, but you can explicitly drop them from tracing at the C-extension level:
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="healthz,metrics,favicon.ico"
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 3. Cap Span Attributes
|
|
604
|
+
|
|
605
|
+
Prevent large, unmanageable spans from consuming excessive memory in the `BatchSpanProcessor`:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT="128"
|
|
609
|
+
export OTEL_SPAN_EVENT_COUNT_LIMIT="128"
|
|
610
|
+
export OTEL_SPAN_LINK_COUNT_LIMIT="128"
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### 4. Optimize Output Buffers
|
|
614
|
+
|
|
615
|
+
The default OpenTelemetry batch limits are too conservative for high-throughput ASGI microservices. Increase the max queue limits so spikes aren't dropped, but decrease the timeout so the process memory is flushed faster:
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
export OTEL_BSP_MAX_QUEUE_SIZE="10000"
|
|
619
|
+
export OTEL_BSP_MAX_EXPORT_BATCH_SIZE="5000"
|
|
620
|
+
export OTEL_BSP_SCHEDULE_DELAY="1000"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
578
625
|
## Examples
|
|
579
626
|
|
|
580
627
|
The `examples/` directory contains runnable demos:
|
|
@@ -729,7 +776,36 @@ install_observability(app, settings, security_policy=SecurityPolicy(log_request_
|
|
|
729
776
|
app.add_middleware(CORSMiddleware, allow_origins=["*"])
|
|
730
777
|
```
|
|
731
778
|
|
|
732
|
-
### Multi-worker Gunicorn
|
|
779
|
+
### Multi-worker Gunicorn
|
|
780
|
+
|
|
781
|
+
> [!WARNING]
|
|
782
|
+
> `--preload-app` is dangerous if visibility is initialized at the module level.
|
|
783
|
+
>
|
|
784
|
+
> When `--preload-app` is used, the application is loaded into the master process *before* forking the worker processes. `fastapi-observer` uses a highly-performant background thread (`QueueListener`) for non-blocking logging, and OpenTelemetry similarly spawns background export threads.
|
|
785
|
+
>
|
|
786
|
+
> Threads **do not survive** a process fork. If you call `install_observability()` at the module level (e.g., right under `app = FastAPI()`), the background threads will be created in the master process, and all workers will silently drop logs because their logging threads never started.
|
|
787
|
+
>
|
|
788
|
+
> **How to fix:** Either remove `--preload-app`, OR initialize observability inside the FastAPI `lifespan` context manager so it starts safely after the fork inside each worker:
|
|
789
|
+
>
|
|
790
|
+
> ```python
|
|
791
|
+
> from contextlib import asynccontextmanager
|
|
792
|
+
> from fastapi import FastAPI
|
|
793
|
+
> from fastapiobserver import ObservabilitySettings, install_observability
|
|
794
|
+
>
|
|
795
|
+
> settings = ObservabilitySettings(service="api")
|
|
796
|
+
>
|
|
797
|
+
> @asynccontextmanager
|
|
798
|
+
> async def lifespan(app: FastAPI):
|
|
799
|
+
> install_observability(app, settings) # Safely initializes per-worker
|
|
800
|
+
> yield
|
|
801
|
+
> # fastapiobserver automatically registers shutdown hooks so no teardown needed here
|
|
802
|
+
>
|
|
803
|
+
> app = FastAPI(lifespan=lifespan)
|
|
804
|
+
> ```
|
|
805
|
+
|
|
806
|
+
#### Prometheus Multiprocess Mode
|
|
807
|
+
|
|
808
|
+
If you are using Prometheus with multiple Gunicorn workers, you must configure a shared metrics directory:
|
|
733
809
|
|
|
734
810
|
```bash
|
|
735
811
|
export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus-metrics
|
|
@@ -501,11 +501,57 @@ kubectl -n observability rollout status deployment/traffic-generator
|
|
|
501
501
|
kubectl -n observability port-forward svc/grafana 3000:3000
|
|
502
502
|
```
|
|
503
503
|
|
|
504
|
-
Open [http://localhost:3000](http://localhost:3000).
|
|
504
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
505
505
|
Full guide: [`kubernetes.md`](kubernetes.md)
|
|
506
506
|
|
|
507
507
|
---
|
|
508
508
|
|
|
509
|
+
## Low-Overhead & Production Tuning (Advanced)
|
|
510
|
+
|
|
511
|
+
`fastapi-observer` integrates natively with the core OpenTelemetry Python SDK, meaning you can aggressively tune its resource usage purely via standard environment variables without altering your application code.
|
|
512
|
+
|
|
513
|
+
For high-throughput services (e.g. `10k+ RPS`), apply these exact variables to minimize the observer footprint:
|
|
514
|
+
|
|
515
|
+
### 1. Head-Based Sampling
|
|
516
|
+
|
|
517
|
+
Tracing 100% of requests is too expensive at scale. You should configure `fastapi-observer` to respect upstream trace flags, while only sampling a fraction of net-new requests:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
# Keep the parent's sample decision if it exists, otherwise sample 5%
|
|
521
|
+
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
|
|
522
|
+
export OTEL_TRACES_SAMPLER_ARG="0.05"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### 2. Exclude Noisy URLs from the SDK
|
|
526
|
+
|
|
527
|
+
Do not waste cycles generating spans for health checks or static assets. `fastapi-observer` will auto-derive metrics exclusions, but you can explicitly drop them from tracing at the C-extension level:
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="healthz,metrics,favicon.ico"
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 3. Cap Span Attributes
|
|
534
|
+
|
|
535
|
+
Prevent large, unmanageable spans from consuming excessive memory in the `BatchSpanProcessor`:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT="128"
|
|
539
|
+
export OTEL_SPAN_EVENT_COUNT_LIMIT="128"
|
|
540
|
+
export OTEL_SPAN_LINK_COUNT_LIMIT="128"
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 4. Optimize Output Buffers
|
|
544
|
+
|
|
545
|
+
The default OpenTelemetry batch limits are too conservative for high-throughput ASGI microservices. Increase the max queue limits so spikes aren't dropped, but decrease the timeout so the process memory is flushed faster:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
export OTEL_BSP_MAX_QUEUE_SIZE="10000"
|
|
549
|
+
export OTEL_BSP_MAX_EXPORT_BATCH_SIZE="5000"
|
|
550
|
+
export OTEL_BSP_SCHEDULE_DELAY="1000"
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
509
555
|
## Examples
|
|
510
556
|
|
|
511
557
|
The `examples/` directory contains runnable demos:
|
|
@@ -660,7 +706,36 @@ install_observability(app, settings, security_policy=SecurityPolicy(log_request_
|
|
|
660
706
|
app.add_middleware(CORSMiddleware, allow_origins=["*"])
|
|
661
707
|
```
|
|
662
708
|
|
|
663
|
-
### Multi-worker Gunicorn
|
|
709
|
+
### Multi-worker Gunicorn
|
|
710
|
+
|
|
711
|
+
> [!WARNING]
|
|
712
|
+
> `--preload-app` is dangerous if visibility is initialized at the module level.
|
|
713
|
+
>
|
|
714
|
+
> When `--preload-app` is used, the application is loaded into the master process *before* forking the worker processes. `fastapi-observer` uses a highly-performant background thread (`QueueListener`) for non-blocking logging, and OpenTelemetry similarly spawns background export threads.
|
|
715
|
+
>
|
|
716
|
+
> Threads **do not survive** a process fork. If you call `install_observability()` at the module level (e.g., right under `app = FastAPI()`), the background threads will be created in the master process, and all workers will silently drop logs because their logging threads never started.
|
|
717
|
+
>
|
|
718
|
+
> **How to fix:** Either remove `--preload-app`, OR initialize observability inside the FastAPI `lifespan` context manager so it starts safely after the fork inside each worker:
|
|
719
|
+
>
|
|
720
|
+
> ```python
|
|
721
|
+
> from contextlib import asynccontextmanager
|
|
722
|
+
> from fastapi import FastAPI
|
|
723
|
+
> from fastapiobserver import ObservabilitySettings, install_observability
|
|
724
|
+
>
|
|
725
|
+
> settings = ObservabilitySettings(service="api")
|
|
726
|
+
>
|
|
727
|
+
> @asynccontextmanager
|
|
728
|
+
> async def lifespan(app: FastAPI):
|
|
729
|
+
> install_observability(app, settings) # Safely initializes per-worker
|
|
730
|
+
> yield
|
|
731
|
+
> # fastapiobserver automatically registers shutdown hooks so no teardown needed here
|
|
732
|
+
>
|
|
733
|
+
> app = FastAPI(lifespan=lifespan)
|
|
734
|
+
> ```
|
|
735
|
+
|
|
736
|
+
#### Prometheus Multiprocess Mode
|
|
737
|
+
|
|
738
|
+
If you are using Prometheus with multiple Gunicorn workers, you must configure a shared metrics directory:
|
|
664
739
|
|
|
665
740
|
```bash
|
|
666
741
|
export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus-metrics
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-observer
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -65,6 +65,7 @@ Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
|
65
65
|
Requires-Dist: pytest-randomly>=3.16.0; extra == "dev"
|
|
66
66
|
Requires-Dist: ruff>=0.15.1; extra == "dev"
|
|
67
67
|
Requires-Dist: cyclonedx-bom>=7.2.1; extra == "dev"
|
|
68
|
+
Requires-Dist: uvicorn>=0.30.0; extra == "dev"
|
|
68
69
|
Dynamic: license-file
|
|
69
70
|
|
|
70
71
|
# fastapi-observer
|
|
@@ -570,11 +571,57 @@ kubectl -n observability rollout status deployment/traffic-generator
|
|
|
570
571
|
kubectl -n observability port-forward svc/grafana 3000:3000
|
|
571
572
|
```
|
|
572
573
|
|
|
573
|
-
Open [http://localhost:3000](http://localhost:3000).
|
|
574
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
574
575
|
Full guide: [`kubernetes.md`](kubernetes.md)
|
|
575
576
|
|
|
576
577
|
---
|
|
577
578
|
|
|
579
|
+
## Low-Overhead & Production Tuning (Advanced)
|
|
580
|
+
|
|
581
|
+
`fastapi-observer` integrates natively with the core OpenTelemetry Python SDK, meaning you can aggressively tune its resource usage purely via standard environment variables without altering your application code.
|
|
582
|
+
|
|
583
|
+
For high-throughput services (e.g. `10k+ RPS`), apply these exact variables to minimize the observer footprint:
|
|
584
|
+
|
|
585
|
+
### 1. Head-Based Sampling
|
|
586
|
+
|
|
587
|
+
Tracing 100% of requests is too expensive at scale. You should configure `fastapi-observer` to respect upstream trace flags, while only sampling a fraction of net-new requests:
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
# Keep the parent's sample decision if it exists, otherwise sample 5%
|
|
591
|
+
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
|
|
592
|
+
export OTEL_TRACES_SAMPLER_ARG="0.05"
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 2. Exclude Noisy URLs from the SDK
|
|
596
|
+
|
|
597
|
+
Do not waste cycles generating spans for health checks or static assets. `fastapi-observer` will auto-derive metrics exclusions, but you can explicitly drop them from tracing at the C-extension level:
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="healthz,metrics,favicon.ico"
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 3. Cap Span Attributes
|
|
604
|
+
|
|
605
|
+
Prevent large, unmanageable spans from consuming excessive memory in the `BatchSpanProcessor`:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT="128"
|
|
609
|
+
export OTEL_SPAN_EVENT_COUNT_LIMIT="128"
|
|
610
|
+
export OTEL_SPAN_LINK_COUNT_LIMIT="128"
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### 4. Optimize Output Buffers
|
|
614
|
+
|
|
615
|
+
The default OpenTelemetry batch limits are too conservative for high-throughput ASGI microservices. Increase the max queue limits so spikes aren't dropped, but decrease the timeout so the process memory is flushed faster:
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
export OTEL_BSP_MAX_QUEUE_SIZE="10000"
|
|
619
|
+
export OTEL_BSP_MAX_EXPORT_BATCH_SIZE="5000"
|
|
620
|
+
export OTEL_BSP_SCHEDULE_DELAY="1000"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
578
625
|
## Examples
|
|
579
626
|
|
|
580
627
|
The `examples/` directory contains runnable demos:
|
|
@@ -729,7 +776,36 @@ install_observability(app, settings, security_policy=SecurityPolicy(log_request_
|
|
|
729
776
|
app.add_middleware(CORSMiddleware, allow_origins=["*"])
|
|
730
777
|
```
|
|
731
778
|
|
|
732
|
-
### Multi-worker Gunicorn
|
|
779
|
+
### Multi-worker Gunicorn
|
|
780
|
+
|
|
781
|
+
> [!WARNING]
|
|
782
|
+
> `--preload-app` is dangerous if visibility is initialized at the module level.
|
|
783
|
+
>
|
|
784
|
+
> When `--preload-app` is used, the application is loaded into the master process *before* forking the worker processes. `fastapi-observer` uses a highly-performant background thread (`QueueListener`) for non-blocking logging, and OpenTelemetry similarly spawns background export threads.
|
|
785
|
+
>
|
|
786
|
+
> Threads **do not survive** a process fork. If you call `install_observability()` at the module level (e.g., right under `app = FastAPI()`), the background threads will be created in the master process, and all workers will silently drop logs because their logging threads never started.
|
|
787
|
+
>
|
|
788
|
+
> **How to fix:** Either remove `--preload-app`, OR initialize observability inside the FastAPI `lifespan` context manager so it starts safely after the fork inside each worker:
|
|
789
|
+
>
|
|
790
|
+
> ```python
|
|
791
|
+
> from contextlib import asynccontextmanager
|
|
792
|
+
> from fastapi import FastAPI
|
|
793
|
+
> from fastapiobserver import ObservabilitySettings, install_observability
|
|
794
|
+
>
|
|
795
|
+
> settings = ObservabilitySettings(service="api")
|
|
796
|
+
>
|
|
797
|
+
> @asynccontextmanager
|
|
798
|
+
> async def lifespan(app: FastAPI):
|
|
799
|
+
> install_observability(app, settings) # Safely initializes per-worker
|
|
800
|
+
> yield
|
|
801
|
+
> # fastapiobserver automatically registers shutdown hooks so no teardown needed here
|
|
802
|
+
>
|
|
803
|
+
> app = FastAPI(lifespan=lifespan)
|
|
804
|
+
> ```
|
|
805
|
+
|
|
806
|
+
#### Prometheus Multiprocess Mode
|
|
807
|
+
|
|
808
|
+
If you are using Prometheus with multiple Gunicorn workers, you must configure a shared metrics directory:
|
|
733
809
|
|
|
734
810
|
```bash
|
|
735
811
|
export PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus-metrics
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2"
|
|
@@ -70,15 +70,15 @@ class RequestLoggingMiddleware:
|
|
|
70
70
|
return
|
|
71
71
|
|
|
72
72
|
start = time.perf_counter()
|
|
73
|
-
|
|
74
|
-
client_ip, trusted_source = self.ip_resolver.resolve(scope,
|
|
75
|
-
request_id = self.context_manager.initialize(
|
|
73
|
+
raw_headers = scope.get("headers", [])
|
|
74
|
+
client_ip, trusted_source = self.ip_resolver.resolve(scope, raw_headers)
|
|
75
|
+
request_id = self.context_manager.initialize(raw_headers, trusted_source)
|
|
76
76
|
|
|
77
77
|
path = scope.get("path", "")
|
|
78
78
|
method = scope.get("method", "UNKNOWN")
|
|
79
79
|
status_code = 500
|
|
80
80
|
captured_error: Exception | None = None
|
|
81
|
-
request_content_type =
|
|
81
|
+
request_content_type = _get_header(raw_headers, b"content-type")
|
|
82
82
|
request_body_enabled = self.security_policy.log_request_body and is_body_capturable(
|
|
83
83
|
request_content_type,
|
|
84
84
|
self.security_policy,
|
|
@@ -102,17 +102,17 @@ class RequestLoggingMiddleware:
|
|
|
102
102
|
nonlocal status_code
|
|
103
103
|
if message["type"] == "http.response.start":
|
|
104
104
|
status_code = int(message.get("status", 500))
|
|
105
|
-
|
|
105
|
+
resp_headers = message.get("headers", [])
|
|
106
106
|
if (
|
|
107
107
|
self.security_policy.log_response_body
|
|
108
108
|
and self.security_policy.body_capture_media_types is not None
|
|
109
109
|
):
|
|
110
|
-
response_content_type =
|
|
110
|
+
response_content_type = _get_header(resp_headers, b"content-type")
|
|
111
111
|
response_body_capture.set_enabled(
|
|
112
112
|
is_body_capturable(response_content_type, self.security_policy)
|
|
113
113
|
)
|
|
114
114
|
updated_headers = _upsert_header(
|
|
115
|
-
|
|
115
|
+
resp_headers,
|
|
116
116
|
self.settings.response_request_id_header,
|
|
117
117
|
request_id,
|
|
118
118
|
)
|
|
@@ -206,7 +206,11 @@ class _IpResolver:
|
|
|
206
206
|
def __init__(self, policy: TrustedProxyPolicy) -> None:
|
|
207
207
|
self.policy = policy
|
|
208
208
|
|
|
209
|
-
def resolve(
|
|
209
|
+
def resolve(
|
|
210
|
+
self,
|
|
211
|
+
scope: Scope,
|
|
212
|
+
headers: list[tuple[bytes, bytes]],
|
|
213
|
+
) -> tuple[str | None, bool]:
|
|
210
214
|
scope_client_ip = _extract_scope_client_ip(scope)
|
|
211
215
|
trusted_source = (
|
|
212
216
|
is_trusted_client_ip(scope_client_ip, self.policy)
|
|
@@ -220,16 +224,16 @@ class _IpResolver:
|
|
|
220
224
|
class _RequestContextManager:
|
|
221
225
|
def __init__(self, settings: ObservabilitySettings) -> None:
|
|
222
226
|
self.settings = settings
|
|
227
|
+
self._request_id_header_bytes = settings.request_id_header.lower().encode("latin1")
|
|
223
228
|
|
|
224
|
-
def initialize(self, headers:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
trusted_source,
|
|
228
|
-
)
|
|
229
|
+
def initialize(self, headers: list[tuple[bytes, bytes]], trusted_source: bool) -> str:
|
|
230
|
+
request_id_header = _get_header(headers, self._request_id_header_bytes)
|
|
231
|
+
request_id = _resolve_request_id(request_id_header, trusted_source)
|
|
229
232
|
set_request_id(request_id)
|
|
230
233
|
|
|
231
234
|
if trusted_source:
|
|
232
|
-
|
|
235
|
+
traceparent = _get_header(headers, b"traceparent")
|
|
236
|
+
trace_id, span_id = _parse_traceparent(traceparent)
|
|
233
237
|
if trace_id:
|
|
234
238
|
set_trace_id(trace_id)
|
|
235
239
|
if span_id:
|
|
@@ -400,11 +404,11 @@ def _upsert_header(
|
|
|
400
404
|
return next_headers
|
|
401
405
|
|
|
402
406
|
|
|
403
|
-
def
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
def _get_header(headers: list[tuple[bytes, bytes]], target: bytes) -> str | None:
|
|
408
|
+
for k, v in headers:
|
|
409
|
+
if k.lower() == target:
|
|
410
|
+
return v.decode("latin1")
|
|
411
|
+
return None
|
|
408
412
|
|
|
409
413
|
|
|
410
414
|
def _classify_error(status_code: int, error: Exception | None) -> str:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
import hashlib
|
|
4
5
|
import ipaddress
|
|
5
6
|
import os
|
|
@@ -464,6 +465,7 @@ def is_body_capturable(content_type: str | None, policy: SecurityPolicy) -> bool
|
|
|
464
465
|
return False
|
|
465
466
|
|
|
466
467
|
|
|
468
|
+
@functools.lru_cache(maxsize=1024)
|
|
467
469
|
def is_trusted_client_ip(client_ip: str | None, policy: TrustedProxyPolicy) -> bool:
|
|
468
470
|
if not policy.enabled:
|
|
469
471
|
return True
|
|
@@ -486,14 +488,20 @@ def is_trusted_client_ip(client_ip: str | None, policy: TrustedProxyPolicy) -> b
|
|
|
486
488
|
|
|
487
489
|
def resolve_client_ip(
|
|
488
490
|
client_ip: str | None,
|
|
489
|
-
headers:
|
|
491
|
+
headers: list[tuple[bytes, bytes]],
|
|
490
492
|
policy: TrustedProxyPolicy,
|
|
491
493
|
) -> str | None:
|
|
492
494
|
if not policy.honor_forwarded_headers:
|
|
493
495
|
return client_ip
|
|
494
496
|
if not is_trusted_client_ip(client_ip, policy):
|
|
495
497
|
return client_ip
|
|
496
|
-
|
|
498
|
+
|
|
499
|
+
forwarded_for = None
|
|
500
|
+
for k, v in headers:
|
|
501
|
+
if k.lower() == b"x-forwarded-for":
|
|
502
|
+
forwarded_for = v.decode("latin1")
|
|
503
|
+
break
|
|
504
|
+
|
|
497
505
|
if not forwarded_for:
|
|
498
506
|
return client_ip
|
|
499
507
|
first_hop = forwarded_for.split(",")[0].strip()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapi_observer.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapi_observer.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/integrations/__init__.py
RENAMED
|
File without changes
|
{fastapi_observer-0.3.1 → fastapi_observer-0.3.2}/src/fastapiobserver/integrations/strawberry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|