fastapi-observer 0.2.5__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {fastapi_observer-0.2.5/src/fastapi_observer.egg-info → fastapi_observer-0.3.0}/PKG-INFO +53 -4
  2. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/README.md +51 -3
  3. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/pyproject.toml +2 -0
  4. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0/src/fastapi_observer.egg-info}/PKG-INFO +53 -4
  5. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapi_observer.egg-info/SOURCES.txt +5 -0
  6. fastapi_observer-0.3.0/src/fastapiobserver/_version.py +1 -0
  7. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/config.py +22 -0
  8. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/control_plane.py +9 -4
  9. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/fastapi.py +19 -4
  10. fastapi_observer-0.3.0/src/fastapiobserver/integrations/__init__.py +0 -0
  11. fastapi_observer-0.3.0/src/fastapiobserver/integrations/strawberry.py +78 -0
  12. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/logging.py +54 -17
  13. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/metrics.py +26 -1
  14. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/middleware.py +2 -2
  15. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/lifecycle.py +3 -5
  16. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/tracing.py +5 -3
  17. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/plugins.py +15 -0
  18. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/request_context.py +15 -0
  19. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/sinks.py +255 -20
  20. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_fastapi_install.py +14 -11
  21. fastapi_observer-0.3.0/tests/test_graphql_integrations.py +69 -0
  22. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_logging_schema_contract.py +41 -4
  23. fastapi_observer-0.3.0/tests/test_logtail_dlq.py +211 -0
  24. fastapi_observer-0.3.0/tests/test_sinks.py +265 -0
  25. fastapi_observer-0.2.5/src/fastapiobserver/_version.py +0 -1
  26. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/LICENSE +0 -0
  27. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/setup.cfg +0 -0
  28. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
  29. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
  30. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
  31. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/__init__.py +0 -0
  32. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/loguru.py +0 -0
  33. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/__init__.py +0 -0
  34. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/logs.py +0 -0
  35. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/metrics.py +0 -0
  36. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/resource.py +0 -0
  37. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/settings.py +0 -0
  38. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/otel/types.py +0 -0
  39. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/propagation.py +0 -0
  40. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/py.typed +0 -0
  41. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/security.py +0 -0
  42. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/src/fastapiobserver/utils.py +0 -0
  43. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_body_media_allowlist.py +0 -0
  44. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_config_settings.py +0 -0
  45. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_excluded_url_precedence.py +0 -0
  46. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_log_queue_controls.py +0 -0
  47. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_loguru_bridge.py +0 -0
  48. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_metrics_optional_dependency.py +0 -0
  49. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_middleware.py +0 -0
  50. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_negotiate_content_type.py +0 -0
  51. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otel_integration.py +0 -0
  52. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otel_lifecycle.py +0 -0
  53. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otel_log_correlation.py +0 -0
  54. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otel_logs_from_env.py +0 -0
  55. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otel_logs_pipeline.py +0 -0
  56. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otel_metrics_from_env.py +0 -0
  57. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_otlp_export_integration.py +0 -0
  58. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_plugin_isolation.py +0 -0
  59. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_propagation.py +0 -0
  60. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_redaction_presets.py +0 -0
  61. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_route_template_cardinality.py +0 -0
  62. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_runtime_control_auth.py +0 -0
  63. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_security_redaction.py +0 -0
  64. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_sink_circuit_breaker.py +0 -0
  65. {fastapi_observer-0.2.5 → fastapi_observer-0.3.0}/tests/test_trust_boundary.py +0 -0
  66. {fastapi_observer-0.2.5 → fastapi_observer-0.3.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: 0.2.5
3
+ Version: 0.3.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
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Framework :: FastAPI
23
23
  Classifier: Topic :: System :: Logging
24
24
  Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
25
26
  Requires-Python: <3.15,>=3.10
26
27
  Description-Content-Type: text/markdown
27
28
  License-File: LICENSE
@@ -423,14 +424,15 @@ Request arrives
423
424
  }
424
425
  ```
425
426
 
426
- On exception logs, a structured `error` object is included for indexed queries:
427
+ On exception logs, a structured `error` object is included for indexed queries, featuring a stable AST-based `fingerprint` hash which ignores transient memory locations or exact line numbers, allowing zero-dependency alerting directly in your search backend.
427
428
 
428
429
  ```json
429
430
  {
430
431
  "error": {
431
432
  "type": "RuntimeError",
432
433
  "message": "boom",
433
- "stacktrace": "Traceback (most recent call last): ..."
434
+ "stacktrace": "Traceback (most recent call last): ...",
435
+ "fingerprint": "a1b2c3d4e5f67890abcd12345678bbcc"
434
436
  }
435
437
  }
436
438
  ```
@@ -583,6 +585,7 @@ The `examples/` directory contains runnable demos:
583
585
  | [`security_presets_app.py`](examples/security_presets_app.py) | Preset-based security policy |
584
586
  | [`allowlist_app.py`](examples/allowlist_app.py) | Allowlist-only sanitization |
585
587
  | [`otel_app.py`](examples/otel_app.py) | OTel tracing and resource attributes |
588
+ | [`graphql_app.py`](examples/graphql_app.py) | Native Strawberry GraphQL observability |
586
589
  | [`benchmarks/`](examples/benchmarks/) | Baseline vs observer benchmark harness |
587
590
  | [`k8s/`](examples/k8s/) | Kubernetes-native stack with Prometheus + Loki + Tempo + Grafana |
588
591
  | [`full_stack/`](examples/full_stack/) | **Docker Compose stack**: 3 FastAPI services + Grafana + Prometheus + Loki + Tempo |
@@ -702,6 +705,13 @@ Notes:
702
705
  | `LOGTAIL_SOURCE_TOKEN` | - | Logtail source token |
703
706
  | `LOGTAIL_BATCH_SIZE` | `50` | Batch size for shipping |
704
707
  | `LOGTAIL_FLUSH_INTERVAL` | `2.0` | Flush interval (seconds) |
708
+ | `LOGTAIL_DLQ_ENABLED` | `false` | Enable resilient local disk fallback for dropped logs |
709
+ | `LOGTAIL_DLQ_DIR` | `.dlq/logtail` | Directory to archive dropped NDJSON messages |
710
+ | `LOGTAIL_DLQ_MAX_BYTES` | `52428800` | Max bytes per DLQ file before rotation (50MB) |
711
+ | `LOGTAIL_DLQ_COMPRESS` | `true` | GZIP compress rotated DLQ files |
712
+
713
+ > [!TIP]
714
+ > The Logtail Dead Letter Queue (DLQ) provides best-effort local durability. If the internal memory queue overflows under immense API pressure (`queue.Full`), or if an external network partition completely exhausts the outbound HTTP retry backoff, the dropped log payloads are immediately salvaged into local NDJSON envelopes. You can replay these files to BetterStack later using the provided `scripts/replay_dlq.py` utility.
705
715
 
706
716
  ---
707
717
 
@@ -818,6 +828,45 @@ Detailed migration/coexistence guide: [`loguru.md`](loguru.md)
818
828
 
819
829
  ---
820
830
 
831
+ ## GraphQL Integrations (Strawberry)
832
+
833
+ If you use `strawberry-graphql`, routing all traffic through `POST /graphql` severely blinds your logs and traces.
834
+ `fastapi-observer` ships a native Strawberry extension (via Duck Typing, meaning NO bloated pip dependencies) to automatically extract GraphQL operations.
835
+
836
+ ```python
837
+ import strawberry
838
+ from fastapiobserver.integrations.strawberry import StrawberryObservabilityExtension
839
+
840
+ @strawberry.type
841
+ class Query:
842
+ @strawberry.field
843
+ def hello(self) -> str:
844
+ return "world"
845
+
846
+ schema = strawberry.Schema(
847
+ query=Query,
848
+ extensions=[StrawberryObservabilityExtension], # Inject this!
849
+ )
850
+ ```
851
+
852
+ With this extension, your logs will automatically get a `graphql` context key containing the extracted `operation_name`:
853
+ ```json
854
+ {
855
+ "event": {
856
+ "method": "POST",
857
+ "path": "/graphql"
858
+ },
859
+ "user_context": {
860
+ "graphql": {
861
+ "operation_name": "GetUsersQuery"
862
+ }
863
+ }
864
+ }
865
+ ```
866
+ If OpenTelemetry is enabled, your traces will dynamically rename from `POST /graphql` to `graphql.operation.GetUsersQuery`.
867
+
868
+ ---
869
+
821
870
  ## Plugin Hooks
822
871
 
823
872
  Extend behavior without editing package internals:
@@ -914,7 +963,7 @@ Reproducible benchmark harness and methodology:
914
963
  - `0.2.x`: OTel interoperability, security presets, allowlists
915
964
  - `1.0.0`: dynamic runtime controls and plugin stability
916
965
 
917
- Current release version: `0.2.5`
966
+ Current release version: `0.3.0`
918
967
 
919
968
  ## Changelog Policy
920
969
 
@@ -355,14 +355,15 @@ Request arrives
355
355
  }
356
356
  ```
357
357
 
358
- On exception logs, a structured `error` object is included for indexed queries:
358
+ On exception logs, a structured `error` object is included for indexed queries, featuring a stable AST-based `fingerprint` hash which ignores transient memory locations or exact line numbers, allowing zero-dependency alerting directly in your search backend.
359
359
 
360
360
  ```json
361
361
  {
362
362
  "error": {
363
363
  "type": "RuntimeError",
364
364
  "message": "boom",
365
- "stacktrace": "Traceback (most recent call last): ..."
365
+ "stacktrace": "Traceback (most recent call last): ...",
366
+ "fingerprint": "a1b2c3d4e5f67890abcd12345678bbcc"
366
367
  }
367
368
  }
368
369
  ```
@@ -515,6 +516,7 @@ The `examples/` directory contains runnable demos:
515
516
  | [`security_presets_app.py`](examples/security_presets_app.py) | Preset-based security policy |
516
517
  | [`allowlist_app.py`](examples/allowlist_app.py) | Allowlist-only sanitization |
517
518
  | [`otel_app.py`](examples/otel_app.py) | OTel tracing and resource attributes |
519
+ | [`graphql_app.py`](examples/graphql_app.py) | Native Strawberry GraphQL observability |
518
520
  | [`benchmarks/`](examples/benchmarks/) | Baseline vs observer benchmark harness |
519
521
  | [`k8s/`](examples/k8s/) | Kubernetes-native stack with Prometheus + Loki + Tempo + Grafana |
520
522
  | [`full_stack/`](examples/full_stack/) | **Docker Compose stack**: 3 FastAPI services + Grafana + Prometheus + Loki + Tempo |
@@ -634,6 +636,13 @@ Notes:
634
636
  | `LOGTAIL_SOURCE_TOKEN` | - | Logtail source token |
635
637
  | `LOGTAIL_BATCH_SIZE` | `50` | Batch size for shipping |
636
638
  | `LOGTAIL_FLUSH_INTERVAL` | `2.0` | Flush interval (seconds) |
639
+ | `LOGTAIL_DLQ_ENABLED` | `false` | Enable resilient local disk fallback for dropped logs |
640
+ | `LOGTAIL_DLQ_DIR` | `.dlq/logtail` | Directory to archive dropped NDJSON messages |
641
+ | `LOGTAIL_DLQ_MAX_BYTES` | `52428800` | Max bytes per DLQ file before rotation (50MB) |
642
+ | `LOGTAIL_DLQ_COMPRESS` | `true` | GZIP compress rotated DLQ files |
643
+
644
+ > [!TIP]
645
+ > The Logtail Dead Letter Queue (DLQ) provides best-effort local durability. If the internal memory queue overflows under immense API pressure (`queue.Full`), or if an external network partition completely exhausts the outbound HTTP retry backoff, the dropped log payloads are immediately salvaged into local NDJSON envelopes. You can replay these files to BetterStack later using the provided `scripts/replay_dlq.py` utility.
637
646
 
638
647
  ---
639
648
 
@@ -750,6 +759,45 @@ Detailed migration/coexistence guide: [`loguru.md`](loguru.md)
750
759
 
751
760
  ---
752
761
 
762
+ ## GraphQL Integrations (Strawberry)
763
+
764
+ If you use `strawberry-graphql`, routing all traffic through `POST /graphql` severely blinds your logs and traces.
765
+ `fastapi-observer` ships a native Strawberry extension (via Duck Typing, meaning NO bloated pip dependencies) to automatically extract GraphQL operations.
766
+
767
+ ```python
768
+ import strawberry
769
+ from fastapiobserver.integrations.strawberry import StrawberryObservabilityExtension
770
+
771
+ @strawberry.type
772
+ class Query:
773
+ @strawberry.field
774
+ def hello(self) -> str:
775
+ return "world"
776
+
777
+ schema = strawberry.Schema(
778
+ query=Query,
779
+ extensions=[StrawberryObservabilityExtension], # Inject this!
780
+ )
781
+ ```
782
+
783
+ With this extension, your logs will automatically get a `graphql` context key containing the extracted `operation_name`:
784
+ ```json
785
+ {
786
+ "event": {
787
+ "method": "POST",
788
+ "path": "/graphql"
789
+ },
790
+ "user_context": {
791
+ "graphql": {
792
+ "operation_name": "GetUsersQuery"
793
+ }
794
+ }
795
+ }
796
+ ```
797
+ If OpenTelemetry is enabled, your traces will dynamically rename from `POST /graphql` to `graphql.operation.GetUsersQuery`.
798
+
799
+ ---
800
+
753
801
  ## Plugin Hooks
754
802
 
755
803
  Extend behavior without editing package internals:
@@ -846,7 +894,7 @@ Reproducible benchmark harness and methodology:
846
894
  - `0.2.x`: OTel interoperability, security presets, allowlists
847
895
  - `1.0.0`: dynamic runtime controls and plugin stability
848
896
 
849
- Current release version: `0.2.5`
897
+ Current release version: `0.3.0`
850
898
 
851
899
  ## Changelog Policy
852
900
 
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Framework :: FastAPI",
25
25
  "Topic :: System :: Logging",
26
26
  "Topic :: System :: Monitoring",
27
+ "Typing :: Typed",
27
28
  ]
28
29
  keywords = ["fastapi", "observability", "logging", "metrics", "opentelemetry"]
29
30
  license = "MIT"
@@ -114,3 +115,4 @@ ignore_missing_imports = true
114
115
  [tool.pytest.ini_options]
115
116
  pythonpath = ["src"]
116
117
  testpaths = ["tests"]
118
+ asyncio_mode = "auto"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-observer
3
- Version: 0.2.5
3
+ Version: 0.3.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
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Framework :: FastAPI
23
23
  Classifier: Topic :: System :: Logging
24
24
  Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
25
26
  Requires-Python: <3.15,>=3.10
26
27
  Description-Content-Type: text/markdown
27
28
  License-File: LICENSE
@@ -423,14 +424,15 @@ Request arrives
423
424
  }
424
425
  ```
425
426
 
426
- On exception logs, a structured `error` object is included for indexed queries:
427
+ On exception logs, a structured `error` object is included for indexed queries, featuring a stable AST-based `fingerprint` hash which ignores transient memory locations or exact line numbers, allowing zero-dependency alerting directly in your search backend.
427
428
 
428
429
  ```json
429
430
  {
430
431
  "error": {
431
432
  "type": "RuntimeError",
432
433
  "message": "boom",
433
- "stacktrace": "Traceback (most recent call last): ..."
434
+ "stacktrace": "Traceback (most recent call last): ...",
435
+ "fingerprint": "a1b2c3d4e5f67890abcd12345678bbcc"
434
436
  }
435
437
  }
436
438
  ```
@@ -583,6 +585,7 @@ The `examples/` directory contains runnable demos:
583
585
  | [`security_presets_app.py`](examples/security_presets_app.py) | Preset-based security policy |
584
586
  | [`allowlist_app.py`](examples/allowlist_app.py) | Allowlist-only sanitization |
585
587
  | [`otel_app.py`](examples/otel_app.py) | OTel tracing and resource attributes |
588
+ | [`graphql_app.py`](examples/graphql_app.py) | Native Strawberry GraphQL observability |
586
589
  | [`benchmarks/`](examples/benchmarks/) | Baseline vs observer benchmark harness |
587
590
  | [`k8s/`](examples/k8s/) | Kubernetes-native stack with Prometheus + Loki + Tempo + Grafana |
588
591
  | [`full_stack/`](examples/full_stack/) | **Docker Compose stack**: 3 FastAPI services + Grafana + Prometheus + Loki + Tempo |
@@ -702,6 +705,13 @@ Notes:
702
705
  | `LOGTAIL_SOURCE_TOKEN` | - | Logtail source token |
703
706
  | `LOGTAIL_BATCH_SIZE` | `50` | Batch size for shipping |
704
707
  | `LOGTAIL_FLUSH_INTERVAL` | `2.0` | Flush interval (seconds) |
708
+ | `LOGTAIL_DLQ_ENABLED` | `false` | Enable resilient local disk fallback for dropped logs |
709
+ | `LOGTAIL_DLQ_DIR` | `.dlq/logtail` | Directory to archive dropped NDJSON messages |
710
+ | `LOGTAIL_DLQ_MAX_BYTES` | `52428800` | Max bytes per DLQ file before rotation (50MB) |
711
+ | `LOGTAIL_DLQ_COMPRESS` | `true` | GZIP compress rotated DLQ files |
712
+
713
+ > [!TIP]
714
+ > The Logtail Dead Letter Queue (DLQ) provides best-effort local durability. If the internal memory queue overflows under immense API pressure (`queue.Full`), or if an external network partition completely exhausts the outbound HTTP retry backoff, the dropped log payloads are immediately salvaged into local NDJSON envelopes. You can replay these files to BetterStack later using the provided `scripts/replay_dlq.py` utility.
705
715
 
706
716
  ---
707
717
 
@@ -818,6 +828,45 @@ Detailed migration/coexistence guide: [`loguru.md`](loguru.md)
818
828
 
819
829
  ---
820
830
 
831
+ ## GraphQL Integrations (Strawberry)
832
+
833
+ If you use `strawberry-graphql`, routing all traffic through `POST /graphql` severely blinds your logs and traces.
834
+ `fastapi-observer` ships a native Strawberry extension (via Duck Typing, meaning NO bloated pip dependencies) to automatically extract GraphQL operations.
835
+
836
+ ```python
837
+ import strawberry
838
+ from fastapiobserver.integrations.strawberry import StrawberryObservabilityExtension
839
+
840
+ @strawberry.type
841
+ class Query:
842
+ @strawberry.field
843
+ def hello(self) -> str:
844
+ return "world"
845
+
846
+ schema = strawberry.Schema(
847
+ query=Query,
848
+ extensions=[StrawberryObservabilityExtension], # Inject this!
849
+ )
850
+ ```
851
+
852
+ With this extension, your logs will automatically get a `graphql` context key containing the extracted `operation_name`:
853
+ ```json
854
+ {
855
+ "event": {
856
+ "method": "POST",
857
+ "path": "/graphql"
858
+ },
859
+ "user_context": {
860
+ "graphql": {
861
+ "operation_name": "GetUsersQuery"
862
+ }
863
+ }
864
+ }
865
+ ```
866
+ If OpenTelemetry is enabled, your traces will dynamically rename from `POST /graphql` to `graphql.operation.GetUsersQuery`.
867
+
868
+ ---
869
+
821
870
  ## Plugin Hooks
822
871
 
823
872
  Extend behavior without editing package internals:
@@ -914,7 +963,7 @@ Reproducible benchmark harness and methodology:
914
963
  - `0.2.x`: OTel interoperability, security presets, allowlists
915
964
  - `1.0.0`: dynamic runtime controls and plugin stability
916
965
 
917
- Current release version: `0.2.5`
966
+ Current release version: `0.3.0`
918
967
 
919
968
  ## Changelog Policy
920
969
 
@@ -22,6 +22,8 @@ src/fastapiobserver/request_context.py
22
22
  src/fastapiobserver/security.py
23
23
  src/fastapiobserver/sinks.py
24
24
  src/fastapiobserver/utils.py
25
+ src/fastapiobserver/integrations/__init__.py
26
+ src/fastapiobserver/integrations/strawberry.py
25
27
  src/fastapiobserver/otel/__init__.py
26
28
  src/fastapiobserver/otel/lifecycle.py
27
29
  src/fastapiobserver/otel/logs.py
@@ -34,8 +36,10 @@ tests/test_body_media_allowlist.py
34
36
  tests/test_config_settings.py
35
37
  tests/test_excluded_url_precedence.py
36
38
  tests/test_fastapi_install.py
39
+ tests/test_graphql_integrations.py
37
40
  tests/test_log_queue_controls.py
38
41
  tests/test_logging_schema_contract.py
42
+ tests/test_logtail_dlq.py
39
43
  tests/test_loguru_bridge.py
40
44
  tests/test_metrics_optional_dependency.py
41
45
  tests/test_middleware.py
@@ -54,5 +58,6 @@ tests/test_route_template_cardinality.py
54
58
  tests/test_runtime_control_auth.py
55
59
  tests/test_security_redaction.py
56
60
  tests/test_sink_circuit_breaker.py
61
+ tests/test_sinks.py
57
62
  tests/test_trust_boundary.py
58
63
  tests/test_utils_refactors.py
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -103,6 +103,28 @@ class ObservabilitySettings(BaseSettings):
103
103
  default=2.0, gt=0.0, validation_alias="LOGTAIL_FLUSH_INTERVAL"
104
104
  )
105
105
 
106
+ # --- logtail DLQ ---
107
+ logtail_dlq_enabled: bool = Field(
108
+ default=False, validation_alias="LOGTAIL_DLQ_ENABLED"
109
+ )
110
+ logtail_dlq_dir: str = Field(
111
+ default=".dlq/logtail", validation_alias="LOGTAIL_DLQ_DIR"
112
+ )
113
+ logtail_dlq_filename: str = Field(
114
+ default="logtail_dlq.ndjson",
115
+ validation_alias="LOGTAIL_DLQ_FILENAME",
116
+ )
117
+ logtail_dlq_max_bytes: int = Field(
118
+ default=50 * 1024 * 1024,
119
+ validation_alias="LOGTAIL_DLQ_MAX_BYTES",
120
+ )
121
+ logtail_dlq_backup_count: int = Field(
122
+ default=10, validation_alias="LOGTAIL_DLQ_BACKUP_COUNT"
123
+ )
124
+ logtail_dlq_compress: bool = Field(
125
+ default=True, validation_alias="LOGTAIL_DLQ_COMPRESS"
126
+ )
127
+
106
128
  # ---------- validators ----------
107
129
 
108
130
  @field_validator("log_level")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  import os
5
5
  import secrets
6
+ import weakref
6
7
  from typing import Annotated, Literal
7
8
 
8
9
  from fastapi import APIRouter, Depends, FastAPI, Header, HTTPException, status
@@ -12,6 +13,13 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
12
13
  from .otel import get_trace_sampling_ratio, set_trace_sampling_ratio
13
14
  from .utils import EnvLoadable, normalize_path
14
15
 
16
+ __all__ = [
17
+ "RuntimeControlSettings",
18
+ "ControlPlanePayload",
19
+ "mount_control_plane",
20
+ ]
21
+
22
+ _MOUNTED_CONTROL_PATHS: weakref.WeakKeyDictionary[FastAPI, set[str]] = weakref.WeakKeyDictionary()
15
23
 
16
24
  class RuntimeControlSettings(EnvLoadable, BaseModel):
17
25
  model_config = ConfigDict(frozen=True)
@@ -57,9 +65,7 @@ def mount_control_plane(app: FastAPI, settings: RuntimeControlSettings) -> None:
57
65
  )
58
66
 
59
67
  normalized_path = settings.path
60
- mounted_paths: set[str] = getattr(
61
- app.state, "_fastapiobserver_control_paths", set()
62
- )
68
+ mounted_paths = _MOUNTED_CONTROL_PATHS.setdefault(app, set())
63
69
  if normalized_path in mounted_paths:
64
70
  return
65
71
 
@@ -93,7 +99,6 @@ def mount_control_plane(app: FastAPI, settings: RuntimeControlSettings) -> None:
93
99
 
94
100
  app.include_router(router)
95
101
  mounted_paths.add(normalized_path)
96
- app.state._fastapiobserver_control_paths = mounted_paths
97
102
 
98
103
 
99
104
  def _current_runtime_settings() -> dict[str, object]:
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import weakref
5
+ import typing
6
+ from contextlib import asynccontextmanager
4
7
  from typing import Literal
5
8
 
6
9
  from fastapi import FastAPI
@@ -24,7 +27,7 @@ from .otel import (
24
27
  from .security import SecurityPolicy, TrustedProxyPolicy
25
28
 
26
29
  _LOGGER = logging.getLogger("fastapiobserver.fastapi")
27
- _LOGGING_SHUTDOWN_HOOK_ATTR = "_fastapiobserver_logging_shutdown_hook_registered"
30
+ _REGISTERED_APPS: weakref.WeakSet[FastAPI] = weakref.WeakSet()
28
31
 
29
32
 
30
33
  def install_observability(
@@ -156,7 +159,19 @@ def _has_request_logging_middleware(app: FastAPI) -> bool:
156
159
 
157
160
 
158
161
  def _register_logging_shutdown_hook(app: FastAPI) -> None:
159
- if bool(getattr(app.state, _LOGGING_SHUTDOWN_HOOK_ATTR, False)):
162
+ if app in _REGISTERED_APPS:
160
163
  return
161
- app.add_event_handler("shutdown", shutdown_logging)
162
- setattr(app.state, _LOGGING_SHUTDOWN_HOOK_ATTR, True)
164
+
165
+ original_lifespan = app.router.lifespan_context
166
+
167
+ @asynccontextmanager
168
+ async def observability_lifespan(app_inst: FastAPI) -> typing.AsyncGenerator[typing.Any, None]:
169
+ if original_lifespan:
170
+ async with original_lifespan(app_inst) as state:
171
+ yield state
172
+ else:
173
+ yield {}
174
+ shutdown_logging()
175
+
176
+ app.router.lifespan_context = observability_lifespan
177
+ _REGISTERED_APPS.add(app)
@@ -0,0 +1,78 @@
1
+ """Strawberry GraphQL integration for fastapi-observer.
2
+
3
+ This module provides a `StrawberryObservabilityExtension` that automatically
4
+ extracts the `operationName` from incoming GraphQL requests and injects it
5
+ into the active `fastapiobserver` telemetry context, as well as setting the
6
+ OpenTelemetry span name if active.
7
+
8
+ It does NOT require `strawberry-graphql` as a hard dependency. It uses Duck Typing.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+ from ..request_context import get_user_context, set_user_context
17
+
18
+ _LOGGER = logging.getLogger("fastapiobserver.integrations.strawberry")
19
+
20
+
21
+ class StrawberryObservabilityExtension:
22
+ """Strawberry SchemaExtension for injecting operation names into logs/traces.
23
+
24
+ Usage:
25
+ import strawberry
26
+ from fastapiobserver.integrations.strawberry import StrawberryObservabilityExtension
27
+
28
+ schema = strawberry.Schema(
29
+ query=Query,
30
+ extensions=[StrawberryObservabilityExtension],
31
+ )
32
+ """
33
+
34
+ def __init__(self, *, execution_context: Any = None) -> None:
35
+ self.execution_context = execution_context
36
+
37
+ async def on_operation(self) -> Any:
38
+ # Note: In Strawberry extensions, `on_operation` is an async generator.
39
+ # We intercept before the operation runs, yield to allow execution, then clean up.
40
+
41
+ op_name = "AnonymousOperation"
42
+
43
+ try:
44
+ if self.execution_context:
45
+ op_name = self.execution_context.operation_name or op_name
46
+ except Exception:
47
+ _LOGGER.debug(
48
+ "strawberry_extension.parse_failed",
49
+ exc_info=True,
50
+ extra={"_skip_enrichers": True},
51
+ )
52
+
53
+ # 1. Update the logging context so StructuredJsonFormatter sees it
54
+ context = get_user_context() or {}
55
+ graphql_data = context.get("graphql", {})
56
+ if isinstance(graphql_data, dict):
57
+ graphql_data["operation_name"] = op_name
58
+ context["graphql"] = graphql_data
59
+ set_user_context(context)
60
+
61
+ # 2. Update OpenTelemetry Trace if active
62
+ try:
63
+ from opentelemetry import trace as otel_trace # type: ignore
64
+
65
+ span = otel_trace.get_current_span()
66
+ if span and span.is_recording():
67
+ span.update_name(f"graphql.operation.{op_name}")
68
+ span.set_attribute("graphql.operation.name", op_name)
69
+ except ImportError:
70
+ pass # OTel not installed
71
+ except Exception:
72
+ _LOGGER.debug(
73
+ "strawberry_extension.otel_failed",
74
+ exc_info=True,
75
+ extra={"_skip_enrichers": True},
76
+ )
77
+
78
+ yield # Allow Strawberry to execute the actual GraphQL resolvers