fastapi-observer 0.3.2__tar.gz → 0.4.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 (112) hide show
  1. {fastapi_observer-0.3.2/src/fastapi_observer.egg-info → fastapi_observer-0.4.0}/PKG-INFO +17 -3
  2. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/README.md +16 -2
  3. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0/src/fastapi_observer.egg-info}/PKG-INFO +17 -3
  4. fastapi_observer-0.4.0/src/fastapi_observer.egg-info/SOURCES.txt +104 -0
  5. fastapi_observer-0.4.0/src/fastapiobserver/_version.py +1 -0
  6. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/control_plane.py +7 -1
  7. fastapi_observer-0.4.0/src/fastapiobserver/logging/__init__.py +36 -0
  8. fastapi_observer-0.4.0/src/fastapiobserver/logging/circuit_breaker.py +158 -0
  9. fastapi_observer-0.4.0/src/fastapiobserver/logging/filters.py +53 -0
  10. fastapi_observer-0.4.0/src/fastapiobserver/logging/formatter.py +157 -0
  11. fastapi_observer-0.4.0/src/fastapiobserver/logging/queueing.py +197 -0
  12. fastapi_observer-0.4.0/src/fastapiobserver/logging/setup.py +239 -0
  13. fastapi_observer-0.4.0/src/fastapiobserver/logging/state.py +17 -0
  14. fastapi_observer-0.4.0/src/fastapiobserver/metrics/__init__.py +41 -0
  15. fastapi_observer-0.4.0/src/fastapiobserver/metrics/builder.py +48 -0
  16. fastapi_observer-0.4.0/src/fastapiobserver/metrics/contracts.py +39 -0
  17. fastapi_observer-0.4.0/src/fastapiobserver/metrics/endpoint.py +176 -0
  18. fastapi_observer-0.4.0/src/fastapiobserver/metrics/noop.py +15 -0
  19. fastapi_observer-0.4.0/src/fastapiobserver/metrics/pathing.py +18 -0
  20. fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/__init__.py +21 -0
  21. fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/backend.py +126 -0
  22. fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/client.py +19 -0
  23. fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/collector.py +170 -0
  24. fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/exemplars.py +19 -0
  25. fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/multiprocess.py +38 -0
  26. fastapi_observer-0.4.0/src/fastapiobserver/metrics/registry.py +42 -0
  27. fastapi_observer-0.4.0/src/fastapiobserver/middleware/__init__.py +27 -0
  28. fastapi_observer-0.4.0/src/fastapiobserver/middleware/body_capture.py +37 -0
  29. fastapi_observer-0.4.0/src/fastapiobserver/middleware/context.py +63 -0
  30. fastapi_observer-0.4.0/src/fastapiobserver/middleware/events.py +81 -0
  31. fastapi_observer-0.4.0/src/fastapiobserver/middleware/headers.py +21 -0
  32. fastapi_observer-0.4.0/src/fastapiobserver/middleware/ip.py +40 -0
  33. fastapi_observer-0.4.0/src/fastapiobserver/middleware/metrics.py +61 -0
  34. fastapi_observer-0.4.0/src/fastapiobserver/middleware/request_logging.py +154 -0
  35. fastapi_observer-0.4.0/src/fastapiobserver/middleware/span_errors.py +34 -0
  36. fastapi_observer-0.4.0/src/fastapiobserver/security/__init__.py +31 -0
  37. fastapi_observer-0.4.0/src/fastapiobserver/security/normalize.py +8 -0
  38. fastapi_observer-0.3.2/src/fastapiobserver/security.py → fastapi_observer-0.4.0/src/fastapiobserver/security/policy.py +19 -203
  39. fastapi_observer-0.4.0/src/fastapiobserver/security/proxies.py +49 -0
  40. fastapi_observer-0.4.0/src/fastapiobserver/security/redaction.py +150 -0
  41. fastapi_observer-0.4.0/src/fastapiobserver/sinks/__init__.py +25 -0
  42. fastapi_observer-0.4.0/src/fastapiobserver/sinks/builtin.py +56 -0
  43. fastapi_observer-0.4.0/src/fastapiobserver/sinks/discovery.py +68 -0
  44. fastapi_observer-0.4.0/src/fastapiobserver/sinks/factory.py +73 -0
  45. fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/__init__.py +5 -0
  46. fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/dlq.py +130 -0
  47. fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/handler.py +205 -0
  48. fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/sink.py +66 -0
  49. fastapi_observer-0.4.0/src/fastapiobserver/sinks/protocol.py +15 -0
  50. fastapi_observer-0.4.0/src/fastapiobserver/sinks/registry.py +33 -0
  51. fastapi_observer-0.4.0/src/fastapiobserver/sinks/stats.py +55 -0
  52. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_log_queue_controls.py +17 -0
  53. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_logging_schema_contract.py +33 -12
  54. fastapi_observer-0.4.0/tests/test_metrics_contracts.py +111 -0
  55. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_metrics_optional_dependency.py +4 -4
  56. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_middleware.py +34 -0
  57. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_sink_circuit_breaker.py +21 -0
  58. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_sinks.py +80 -36
  59. fastapi_observer-0.3.2/src/fastapi_observer.egg-info/SOURCES.txt +0 -63
  60. fastapi_observer-0.3.2/src/fastapiobserver/_version.py +0 -1
  61. fastapi_observer-0.3.2/src/fastapiobserver/logging.py +0 -825
  62. fastapi_observer-0.3.2/src/fastapiobserver/metrics.py +0 -668
  63. fastapi_observer-0.3.2/src/fastapiobserver/middleware.py +0 -447
  64. fastapi_observer-0.3.2/src/fastapiobserver/sinks.py +0 -677
  65. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/LICENSE +0 -0
  66. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/pyproject.toml +0 -0
  67. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/setup.cfg +0 -0
  68. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
  69. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
  70. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
  71. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/__init__.py +0 -0
  72. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/config.py +0 -0
  73. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/fastapi.py +0 -0
  74. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/integrations/__init__.py +0 -0
  75. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
  76. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/loguru.py +0 -0
  77. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/__init__.py +0 -0
  78. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/lifecycle.py +0 -0
  79. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/logs.py +0 -0
  80. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/metrics.py +0 -0
  81. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/resource.py +0 -0
  82. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/settings.py +0 -0
  83. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/tracing.py +0 -0
  84. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/types.py +0 -0
  85. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/plugins.py +0 -0
  86. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/propagation.py +0 -0
  87. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/py.typed +0 -0
  88. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/request_context.py +0 -0
  89. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/utils.py +0 -0
  90. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_body_media_allowlist.py +0 -0
  91. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_config_settings.py +0 -0
  92. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_excluded_url_precedence.py +0 -0
  93. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_fastapi_install.py +0 -0
  94. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_graphql_integrations.py +0 -0
  95. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_logtail_dlq.py +0 -0
  96. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_loguru_bridge.py +0 -0
  97. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_negotiate_content_type.py +0 -0
  98. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_integration.py +0 -0
  99. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_lifecycle.py +0 -0
  100. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_log_correlation.py +0 -0
  101. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_logs_from_env.py +0 -0
  102. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_logs_pipeline.py +0 -0
  103. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_metrics_from_env.py +0 -0
  104. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otlp_export_integration.py +0 -0
  105. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_plugin_isolation.py +0 -0
  106. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_propagation.py +0 -0
  107. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_redaction_presets.py +0 -0
  108. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_route_template_cardinality.py +0 -0
  109. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_runtime_control_auth.py +0 -0
  110. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_security_redaction.py +0 -0
  111. {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_trust_boundary.py +0 -0
  112. {fastapi_observer-0.3.2 → fastapi_observer-0.4.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.3.2
3
+ Version: 0.4.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
@@ -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
@@ -1037,9 +1050,10 @@ Reproducible benchmark harness and methodology:
1037
1050
 
1038
1051
  - `0.1.x`: secure-by-default core
1039
1052
  - `0.2.x`: OTel interoperability, security presets, allowlists
1053
+ - `0.3.x`: package modularization, sink/metrics hardening, and performance reliability updates
1040
1054
  - `1.0.0`: dynamic runtime controls and plugin stability
1041
1055
 
1042
- Current release version: `0.3.1`
1056
+ Current release version: `0.4.0`
1043
1057
 
1044
1058
  ## Changelog Policy
1045
1059
 
@@ -1094,4 +1108,4 @@ The pre-push hook runs:
1094
1108
 
1095
1109
  ## Roadmap Tracking
1096
1110
 
1097
- See [NEXT_STEPS.md](NEXT_STEPS.md) for the active `0.2.0` roadmap and release checklist.
1111
+ 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
@@ -967,9 +980,10 @@ Reproducible benchmark harness and methodology:
967
980
 
968
981
  - `0.1.x`: secure-by-default core
969
982
  - `0.2.x`: OTel interoperability, security presets, allowlists
983
+ - `0.3.x`: package modularization, sink/metrics hardening, and performance reliability updates
970
984
  - `1.0.0`: dynamic runtime controls and plugin stability
971
985
 
972
- Current release version: `0.3.1`
986
+ Current release version: `0.4.0`
973
987
 
974
988
  ## Changelog Policy
975
989
 
@@ -1024,4 +1038,4 @@ The pre-push hook runs:
1024
1038
 
1025
1039
  ## Roadmap Tracking
1026
1040
 
1027
- See [NEXT_STEPS.md](NEXT_STEPS.md) for the active `0.2.0` roadmap and release checklist.
1041
+ 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.2
3
+ Version: 0.4.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
@@ -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
@@ -1037,9 +1050,10 @@ Reproducible benchmark harness and methodology:
1037
1050
 
1038
1051
  - `0.1.x`: secure-by-default core
1039
1052
  - `0.2.x`: OTel interoperability, security presets, allowlists
1053
+ - `0.3.x`: package modularization, sink/metrics hardening, and performance reliability updates
1040
1054
  - `1.0.0`: dynamic runtime controls and plugin stability
1041
1055
 
1042
- Current release version: `0.3.1`
1056
+ Current release version: `0.4.0`
1043
1057
 
1044
1058
  ## Changelog Policy
1045
1059
 
@@ -1094,4 +1108,4 @@ The pre-push hook runs:
1094
1108
 
1095
1109
  ## Roadmap Tracking
1096
1110
 
1097
- See [NEXT_STEPS.md](NEXT_STEPS.md) for the active `0.2.0` roadmap and release checklist.
1111
+ See [NEXT_STEPS.md](NEXT_STEPS.md) for the active roadmap and release checklist.
@@ -0,0 +1,104 @@
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_excluded_url_precedence.py
78
+ tests/test_fastapi_install.py
79
+ tests/test_graphql_integrations.py
80
+ tests/test_log_queue_controls.py
81
+ tests/test_logging_schema_contract.py
82
+ tests/test_logtail_dlq.py
83
+ tests/test_loguru_bridge.py
84
+ tests/test_metrics_contracts.py
85
+ tests/test_metrics_optional_dependency.py
86
+ tests/test_middleware.py
87
+ tests/test_negotiate_content_type.py
88
+ tests/test_otel_integration.py
89
+ tests/test_otel_lifecycle.py
90
+ tests/test_otel_log_correlation.py
91
+ tests/test_otel_logs_from_env.py
92
+ tests/test_otel_logs_pipeline.py
93
+ tests/test_otel_metrics_from_env.py
94
+ tests/test_otlp_export_integration.py
95
+ tests/test_plugin_isolation.py
96
+ tests/test_propagation.py
97
+ tests/test_redaction_presets.py
98
+ tests/test_route_template_cardinality.py
99
+ tests/test_runtime_control_auth.py
100
+ tests/test_security_redaction.py
101
+ tests/test_sink_circuit_breaker.py
102
+ tests/test_sinks.py
103
+ tests/test_trust_boundary.py
104
+ tests/test_utils_refactors.py
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -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
- expected = f"Bearer {token}"
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)
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ import re
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Callable, Mapping
9
+
10
+ from .._version import __version__
11
+ from ..config import ObservabilitySettings
12
+ from ..plugins import apply_log_enrichers
13
+ from ..request_context import get_request_id, get_span_id, get_trace_id, get_user_context
14
+ from ..security import SecurityPolicy, sanitize_event
15
+ from ..utils import lazy_import
16
+
17
+ orjson: Any
18
+ try:
19
+ orjson = lazy_import("orjson")
20
+ except ModuleNotFoundError: # pragma: no cover
21
+ orjson = None
22
+
23
+ LOG_SCHEMA_VERSION = "1.0.0"
24
+
25
+
26
+ class StructuredJsonFormatter(logging.Formatter):
27
+ """Serialize log records into structured JSON.
28
+
29
+ Fields include service identity, request ID, trace/span IDs for
30
+ correlation, and user context. Plugin enrichers are applied unless
31
+ the record has ``_skip_enrichers`` set.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ settings: ObservabilitySettings,
37
+ security_policy: SecurityPolicy | None = None,
38
+ *,
39
+ enrich_event: (
40
+ Callable[[dict[str, object]], Mapping[str, object] | dict[str, object]] | None
41
+ ) = None,
42
+ sanitize_payload: (
43
+ Callable[[dict[str, Any], SecurityPolicy], Mapping[str, Any] | dict[str, Any]]
44
+ | None
45
+ ) = None,
46
+ ) -> None:
47
+ super().__init__()
48
+ self.settings = settings
49
+ self.security_policy = security_policy or SecurityPolicy()
50
+ self._enrich_event = enrich_event or apply_log_enrichers
51
+ self._sanitize_payload = sanitize_payload or sanitize_event
52
+
53
+ def format(self, record: logging.LogRecord) -> str:
54
+ payload: dict[str, Any] = {
55
+ "timestamp": datetime.now(timezone.utc).isoformat(),
56
+ "level": record.levelname,
57
+ "logger": record.name,
58
+ "message": record.getMessage(),
59
+ "app_name": self.settings.app_name,
60
+ "service": self.settings.service,
61
+ "environment": self.settings.environment,
62
+ "version": self.settings.version,
63
+ "log_schema_version": LOG_SCHEMA_VERSION,
64
+ "library": "fastapiobserver",
65
+ "library_version": __version__,
66
+ }
67
+
68
+ request_id = getattr(record, "request_id", None) or get_request_id()
69
+ if request_id:
70
+ payload["request_id"] = request_id
71
+
72
+ trace_id = getattr(record, "trace_id", None) or get_trace_id()
73
+ if trace_id:
74
+ payload["trace_id"] = trace_id
75
+
76
+ span_id = getattr(record, "span_id", None) or get_span_id()
77
+ if span_id:
78
+ payload["span_id"] = span_id
79
+
80
+ user_context = get_user_context()
81
+ if user_context:
82
+ payload["user_context"] = user_context
83
+
84
+ event = getattr(record, "event", None)
85
+ if isinstance(event, dict):
86
+ payload["event"] = event
87
+
88
+ if record.exc_info:
89
+ error_payload = _build_structured_error(record, self)
90
+ payload["error"] = error_payload
91
+ # Backward-compatible field retained for existing dashboards.
92
+ payload["exc_info"] = error_payload["stacktrace"]
93
+
94
+ if getattr(record, "_skip_enrichers", False):
95
+ enriched_payload = payload
96
+ else:
97
+ candidate_payload = self._enrich_event(dict(payload))
98
+ if isinstance(candidate_payload, Mapping):
99
+ enriched_payload = dict(candidate_payload)
100
+ else:
101
+ enriched_payload = payload
102
+ sanitized_candidate = self._sanitize_payload(enriched_payload, self.security_policy)
103
+ if isinstance(sanitized_candidate, Mapping):
104
+ sanitized_payload = dict(sanitized_candidate)
105
+ else:
106
+ sanitized_payload = enriched_payload
107
+ return _json_dumps(sanitized_payload)
108
+
109
+
110
+ def _json_dumps(payload: dict[str, Any]) -> str:
111
+ if orjson is not None:
112
+ return orjson.dumps(payload).decode("utf-8")
113
+ return json.dumps(payload, ensure_ascii=True, default=str)
114
+
115
+
116
+ def _build_structured_error(
117
+ record: logging.LogRecord,
118
+ formatter: logging.Formatter,
119
+ ) -> dict[str, str]:
120
+ exc_info = record.exc_info
121
+ if not exc_info:
122
+ return {"type": "", "message": "", "stacktrace": ""}
123
+
124
+ if not isinstance(exc_info, tuple) or len(exc_info) != 3:
125
+ return {
126
+ "type": "Exception",
127
+ "message": "",
128
+ "stacktrace": "",
129
+ }
130
+
131
+ exc_type, exc_value, _exc_tb = exc_info
132
+ error_type = exc_type.__name__ if exc_type is not None else "Exception"
133
+ error_message = str(exc_value) if exc_value is not None else ""
134
+ stacktrace = formatter.formatException(exc_info)
135
+ fingerprint = _generate_error_fingerprint(error_type, stacktrace)
136
+
137
+ return {
138
+ "type": error_type,
139
+ "message": error_message,
140
+ "stacktrace": stacktrace,
141
+ "fingerprint": fingerprint,
142
+ }
143
+
144
+
145
+ def _generate_error_fingerprint(error_type: str, stacktrace: str) -> str:
146
+ """Hash stack trace after stripping out environment-specific noise."""
147
+ if not stacktrace:
148
+ return hashlib.md5(error_type.encode("utf-8")).hexdigest()
149
+
150
+ # Strip hexadecimal memory addresses
151
+ sanitized = re.sub(r"0x[0-9a-fA-F]+", "0x<ptr>", stacktrace)
152
+ # Strip exact line numbers to survive minor file refactoring
153
+ sanitized = re.sub(r"line \d+", "line <N>", sanitized)
154
+
155
+ # Combine error type and sanitized trace to ensure distinct groupings
156
+ payload = f"{error_type}:{sanitized}"
157
+ return hashlib.md5(payload.encode("utf-8")).hexdigest()