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.
- {fastapi_observer-0.3.2/src/fastapi_observer.egg-info → fastapi_observer-0.4.0}/PKG-INFO +17 -3
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/README.md +16 -2
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0/src/fastapi_observer.egg-info}/PKG-INFO +17 -3
- fastapi_observer-0.4.0/src/fastapi_observer.egg-info/SOURCES.txt +104 -0
- fastapi_observer-0.4.0/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/control_plane.py +7 -1
- fastapi_observer-0.4.0/src/fastapiobserver/logging/__init__.py +36 -0
- fastapi_observer-0.4.0/src/fastapiobserver/logging/circuit_breaker.py +158 -0
- fastapi_observer-0.4.0/src/fastapiobserver/logging/filters.py +53 -0
- fastapi_observer-0.4.0/src/fastapiobserver/logging/formatter.py +157 -0
- fastapi_observer-0.4.0/src/fastapiobserver/logging/queueing.py +197 -0
- fastapi_observer-0.4.0/src/fastapiobserver/logging/setup.py +239 -0
- fastapi_observer-0.4.0/src/fastapiobserver/logging/state.py +17 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/__init__.py +41 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/builder.py +48 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/contracts.py +39 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/endpoint.py +176 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/noop.py +15 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/pathing.py +18 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/__init__.py +21 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/backend.py +126 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/client.py +19 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/collector.py +170 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/exemplars.py +19 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/prometheus/multiprocess.py +38 -0
- fastapi_observer-0.4.0/src/fastapiobserver/metrics/registry.py +42 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/__init__.py +27 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/body_capture.py +37 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/context.py +63 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/events.py +81 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/headers.py +21 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/ip.py +40 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/metrics.py +61 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/request_logging.py +154 -0
- fastapi_observer-0.4.0/src/fastapiobserver/middleware/span_errors.py +34 -0
- fastapi_observer-0.4.0/src/fastapiobserver/security/__init__.py +31 -0
- fastapi_observer-0.4.0/src/fastapiobserver/security/normalize.py +8 -0
- fastapi_observer-0.3.2/src/fastapiobserver/security.py → fastapi_observer-0.4.0/src/fastapiobserver/security/policy.py +19 -203
- fastapi_observer-0.4.0/src/fastapiobserver/security/proxies.py +49 -0
- fastapi_observer-0.4.0/src/fastapiobserver/security/redaction.py +150 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/__init__.py +25 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/builtin.py +56 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/discovery.py +68 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/factory.py +73 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/__init__.py +5 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/dlq.py +130 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/handler.py +205 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/logtail/sink.py +66 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/protocol.py +15 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/registry.py +33 -0
- fastapi_observer-0.4.0/src/fastapiobserver/sinks/stats.py +55 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_log_queue_controls.py +17 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_logging_schema_contract.py +33 -12
- fastapi_observer-0.4.0/tests/test_metrics_contracts.py +111 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_metrics_optional_dependency.py +4 -4
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_middleware.py +34 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_sink_circuit_breaker.py +21 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_sinks.py +80 -36
- fastapi_observer-0.3.2/src/fastapi_observer.egg-info/SOURCES.txt +0 -63
- fastapi_observer-0.3.2/src/fastapiobserver/_version.py +0 -1
- fastapi_observer-0.3.2/src/fastapiobserver/logging.py +0 -825
- fastapi_observer-0.3.2/src/fastapiobserver/metrics.py +0 -668
- fastapi_observer-0.3.2/src/fastapiobserver/middleware.py +0 -447
- fastapi_observer-0.3.2/src/fastapiobserver/sinks.py +0 -677
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/LICENSE +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/pyproject.toml +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/setup.cfg +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/fastapi.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/__init__.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/lifecycle.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/logs.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/metrics.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/resource.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/tracing.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/otel/types.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/request_context.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_config_settings.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_excluded_url_precedence.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_negotiate_content_type.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_integration.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_lifecycle.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_log_correlation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_logs_pipeline.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_otlp_export_integration.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_propagation.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_route_template_cardinality.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-0.3.2 → fastapi_observer-0.4.0}/tests/test_trust_boundary.py +0 -0
- {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
|
+
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
+
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.
|
|
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
|
|
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
|
-
|
|
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()
|