sincpro-framework 3.2.0__tar.gz → 3.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 (46) hide show
  1. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/PKG-INFO +62 -5
  2. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/README.md +58 -3
  3. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/pyproject.toml +9 -4
  4. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/bus.py +38 -4
  5. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/bus.pyi +9 -1
  6. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/conf/sincpro_framework_conf.yml +1 -0
  7. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_conf.py +1 -0
  8. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/tracing/__init__.py +4 -1
  9. sincpro_framework-3.3.0/sincpro_framework/tracing/instrumentation.py +122 -0
  10. sincpro_framework-3.3.0/sincpro_framework/tracing/provider.py +116 -0
  11. sincpro_framework-3.3.0/sincpro_framework/tracing/sentry.py +234 -0
  12. sincpro_framework-3.3.0/sincpro_framework/tracing/status.py +35 -0
  13. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/use_bus.py +125 -9
  14. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/use_bus.pyi +15 -1
  15. sincpro_framework-3.2.0/sincpro_framework/tracing/instrumentation.py +0 -77
  16. sincpro_framework-3.2.0/sincpro_framework/tracing/provider.py +0 -91
  17. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/LICENSE.md +0 -0
  18. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/__init__.py +0 -0
  19. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/__init__.py +0 -0
  20. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/framework_context.py +0 -0
  21. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/framework_context.pyi +0 -0
  22. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/mixin.py +0 -0
  23. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/ddd/__init__.py +0 -0
  24. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/ddd/value_object.py +0 -0
  25. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/error_handler.py +0 -0
  26. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/exceptions.py +0 -0
  27. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/__init__.py +0 -0
  28. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/domain/__init__.py +0 -0
  29. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/domain/extractor.py +0 -0
  30. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/domain/models.py +0 -0
  31. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/__init__.py +0 -0
  32. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/framework_docs_extractor.py +0 -0
  33. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/json_schema_generator.py +0 -0
  34. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/mkdocs_markdown_generator.py +0 -0
  35. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/mkdocs_yaml_generator.py +0 -0
  36. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/sincpro_introspector.py +0 -0
  37. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/static_site_generator.py +0 -0
  38. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/service.py +0 -0
  39. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/sincpro_framework_ai_guide.json +0 -0
  40. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/ioc.py +0 -0
  41. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/middleware.py +0 -0
  42. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/py.typed +0 -0
  43. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_abstractions.py +0 -0
  44. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_abstractions.pyi +0 -0
  45. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_logger.py +0 -0
  46. {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/tracing/span_context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sincpro-framework
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: Sincpro framework to use DDD, Clean architecture, Hexagonal architecture
5
5
  License: MIT
6
6
  License-File: LICENSE.md
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
15
  Provides-Extra: opentelemetry
16
+ Provides-Extra: sentry
16
17
  Requires-Dist: dependency-injector (>=4.48.3,<5.0.0)
17
18
  Requires-Dist: opentelemetry-api (>=1.20) ; extra == "opentelemetry"
18
19
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.20) ; extra == "opentelemetry"
@@ -20,7 +21,8 @@ Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.20) ; extra == "opent
20
21
  Requires-Dist: opentelemetry-sdk (>=1.20) ; extra == "opentelemetry"
21
22
  Requires-Dist: pydantic (>=2.12.5,<3.0.0)
22
23
  Requires-Dist: pyyaml (>=6.0.1)
23
- Requires-Dist: sincpro-log (>=1.0.1,<2.0.0)
24
+ Requires-Dist: sentry-sdk (>=2.0) ; extra == "sentry"
25
+ Requires-Dist: sincpro-log (>=1.1.1,<2.0.0)
24
26
  Description-Content-Type: text/markdown
25
27
 
26
28
  # 🚀 Sincpro Framework: Application Layer Framework within Hexagonal Architecture
@@ -104,7 +106,7 @@ Now you are ready to explore more complex use cases! 🚀
104
106
  - [Example of Executing a Use Case](#example-of-executing-a-use-case)
105
107
  9. [Summary](#summary)
106
108
  10. [Middleware System](#middleware-system-1)
107
- 11. [Observability & Tracing](#observability--tracing)
109
+ 11. [Observability](#observability) — tracing (OTLP) + errors (Sentry/GlitchTip)
108
110
  12. [Configuration or settings](#configuration-or-settings)
109
111
  13. [Variables](#variables)
110
112
 
@@ -214,6 +216,12 @@ class PaymentFeature(Feature):
214
216
  - Uses type hints to enhance code quality and support features like autocompletion and type checking.
215
217
  - Improves development efficiency and reliability.
216
218
 
219
+ ### Observability (tracing + errors)
220
+
221
+ - **Tracing** (optional): OpenTelemetry spans on every DTO, export via OTLP (`sincpro-framework[opentelemetry]` + `OTEL_EXPORTER_OTLP_ENDPOINT`).
222
+ - **Errors** (optional): Sentry/GlitchTip capture on bus exceptions (`sincpro-framework[sentry]` + `SENTRY_PYTHON_DSN` in conf). Isolated client — does not call `sentry_sdk.init()`, does not reuse Odoo's client.
223
+ - Independent: you can enable traces, errors, both, or neither.
224
+
217
225
  ## ⚙️ Features vs. Application Service
218
226
 
219
227
  - **Feature**: Represents a discrete, self-contained use case focused on specific functionality, easy to develop and
@@ -1068,9 +1076,17 @@ The JSON schema format enables powerful AI integrations:
1068
1076
  - **Analysis**: AI can identify optimization opportunities and suggest improvements
1069
1077
  - **Migration**: AI can understand dependencies for migration planning
1070
1078
 
1071
- ## Observability & Tracing
1079
+ ## Observability
1080
+
1081
+ The bus always instruments. Extras and env vars only decide **where** data goes.
1072
1082
 
1073
- The framework provides built-in log correlation out of the box and optional distributed tracing via OpenTelemetry.
1083
+ | Signal | Extra | Env | Backend |
1084
+ |---|---|---|---|
1085
+ | Logs (`trace_id` / `span_id`) | none | — | stdout / your logger |
1086
+ | Tracing (spans) | `[opentelemetry]` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Tempo / Jaeger |
1087
+ | Errors (exceptions) | `[sentry]` | `SENTRY_PYTHON_DSN` (framework conf) | GlitchTip / Sentry |
1088
+
1089
+ Missing extra or missing DSN in conf → no-op, the bus still raises. Framework events are independent from the host: Odoo may also capture the same exception with its own release. That is intended.
1074
1090
 
1075
1091
  ### What works without any extra install
1076
1092
 
@@ -1096,6 +1112,46 @@ This installs:
1096
1112
  - `opentelemetry-exporter-otlp-proto-grpc` (primary)
1097
1113
  - `opentelemetry-exporter-otlp-proto-http` (fallback)
1098
1114
 
1115
+ ### Sentry / GlitchTip (errors)
1116
+
1117
+ Same silent contract as OTel. The bus **always** tries to report exceptions; if `sentry-sdk` is missing or the DSN in conf is unset, it is a no-op.
1118
+
1119
+ ```bash
1120
+ pip install sincpro-framework[sentry]
1121
+ export SENTRY_PYTHON_DSN=https://KEY@glitchtip.sincpro.dev/1
1122
+ ```
1123
+
1124
+ Conf (`sincpro_framework/conf/sincpro_framework_conf.yml`) resolves `sentry_dsn` from `SENTRY_PYTHON_DSN`. If that env is set, `observability_status()["sentry"]` is `on:init`, not `off`. The framework does **not** call `sentry_sdk.init()` and does not reuse the host client.
1125
+
1126
+ Each framework event uses an isolated Sentry `Client` whose `release` is computed once at `UseFramework` init: `{app_name}:{library_version}` (Poetry/installed dist). Not `APP_RELEASE`, not the Python version. Example: `payment-cybersource:5.0.3`. Framework-internal errors use `sincpro-framework:<framework version>`.
1127
+
1128
+ Odoo may capture the same exception with Odoo's release. That second event is intended — two products, two releases, same traceback.
1129
+
1130
+ The bus reports **before** the error handler runs. A handler that swallows an unexpected exception still produces a GlitchTip event. Expected domain errors can be excluded per instance:
1131
+
1132
+ ```python
1133
+ app = UseFramework("payment-cybersource") # release auto-detected from the caller package
1134
+ app.ignore_sentry_exceptions(ValidationError, InsufficientFunds)
1135
+ ```
1136
+
1137
+ Pass `package="sincpro-payments-sdk"` to `UseFramework` when the caller is not the library itself (tests, a thin adapter).
1138
+
1139
+ Observability is optional and must never break the bus. After `build_root_bus()` (or the first `app(dto)` call) every instance exposes a probe:
1140
+
1141
+ ```python
1142
+ status = app.observability_status()
1143
+ status["sentry"] # {active, state, reason} state: off | on | failed
1144
+ status["otel"]
1145
+ ```
1146
+
1147
+ - `off` — extra not installed or conf DSN missing (`sdk_missing`, `dsn_missing`)
1148
+ - `on` — isolated client ready (`init`)
1149
+ - `failed` — DSN present but client construction broke; the bus still runs. Logged as **warning**.
1150
+
1151
+ The instance logger emits: `observability sentry=on:init otel=off:no_endpoint`.
1152
+
1153
+ Do not send traces to GlitchTip (`traces_sample_rate=0`); Tempo stays on OTLP.
1154
+
1099
1155
  ### What OpenTelemetry adds
1100
1156
 
1101
1157
  | Without OTel | With OTel |
@@ -1267,6 +1323,7 @@ where you can define some behavior currently we support the following settings:
1267
1323
 
1268
1324
  - `sincpro_framework_log_level`: Log level for the framework logger. Default: `DEBUG`.
1269
1325
  - `otlp_endpoint`: OTLP exporter endpoint for distributed tracing. Resolved from `OTEL_EXPORTER_OTLP_ENDPOINT` env var. Default: `null` (tracing disabled). Requires `sincpro-framework[opentelemetry]`.
1326
+ - `sentry_dsn`: GlitchTip/Sentry DSN. Resolved from `SENTRY_PYTHON_DSN`. Default: `null` (error reporting disabled). Requires `sentry-sdk` (or `sincpro-framework[sentry]`). The framework uses an isolated client with `release={app_name}:{library_version}` and never calls `sentry_sdk.init()`. Odoo may capture the same error separately. Use `UseFramework.ignore_sentry_exceptions(...)` for expected errors.
1270
1327
 
1271
1328
  Override the config file using another
1272
1329
 
@@ -79,7 +79,7 @@ Now you are ready to explore more complex use cases! 🚀
79
79
  - [Example of Executing a Use Case](#example-of-executing-a-use-case)
80
80
  9. [Summary](#summary)
81
81
  10. [Middleware System](#middleware-system-1)
82
- 11. [Observability & Tracing](#observability--tracing)
82
+ 11. [Observability](#observability) — tracing (OTLP) + errors (Sentry/GlitchTip)
83
83
  12. [Configuration or settings](#configuration-or-settings)
84
84
  13. [Variables](#variables)
85
85
 
@@ -189,6 +189,12 @@ class PaymentFeature(Feature):
189
189
  - Uses type hints to enhance code quality and support features like autocompletion and type checking.
190
190
  - Improves development efficiency and reliability.
191
191
 
192
+ ### Observability (tracing + errors)
193
+
194
+ - **Tracing** (optional): OpenTelemetry spans on every DTO, export via OTLP (`sincpro-framework[opentelemetry]` + `OTEL_EXPORTER_OTLP_ENDPOINT`).
195
+ - **Errors** (optional): Sentry/GlitchTip capture on bus exceptions (`sincpro-framework[sentry]` + `SENTRY_PYTHON_DSN` in conf). Isolated client — does not call `sentry_sdk.init()`, does not reuse Odoo's client.
196
+ - Independent: you can enable traces, errors, both, or neither.
197
+
192
198
  ## ⚙️ Features vs. Application Service
193
199
 
194
200
  - **Feature**: Represents a discrete, self-contained use case focused on specific functionality, easy to develop and
@@ -1043,9 +1049,17 @@ The JSON schema format enables powerful AI integrations:
1043
1049
  - **Analysis**: AI can identify optimization opportunities and suggest improvements
1044
1050
  - **Migration**: AI can understand dependencies for migration planning
1045
1051
 
1046
- ## Observability & Tracing
1052
+ ## Observability
1053
+
1054
+ The bus always instruments. Extras and env vars only decide **where** data goes.
1047
1055
 
1048
- The framework provides built-in log correlation out of the box and optional distributed tracing via OpenTelemetry.
1056
+ | Signal | Extra | Env | Backend |
1057
+ |---|---|---|---|
1058
+ | Logs (`trace_id` / `span_id`) | none | — | stdout / your logger |
1059
+ | Tracing (spans) | `[opentelemetry]` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Tempo / Jaeger |
1060
+ | Errors (exceptions) | `[sentry]` | `SENTRY_PYTHON_DSN` (framework conf) | GlitchTip / Sentry |
1061
+
1062
+ Missing extra or missing DSN in conf → no-op, the bus still raises. Framework events are independent from the host: Odoo may also capture the same exception with its own release. That is intended.
1049
1063
 
1050
1064
  ### What works without any extra install
1051
1065
 
@@ -1071,6 +1085,46 @@ This installs:
1071
1085
  - `opentelemetry-exporter-otlp-proto-grpc` (primary)
1072
1086
  - `opentelemetry-exporter-otlp-proto-http` (fallback)
1073
1087
 
1088
+ ### Sentry / GlitchTip (errors)
1089
+
1090
+ Same silent contract as OTel. The bus **always** tries to report exceptions; if `sentry-sdk` is missing or the DSN in conf is unset, it is a no-op.
1091
+
1092
+ ```bash
1093
+ pip install sincpro-framework[sentry]
1094
+ export SENTRY_PYTHON_DSN=https://KEY@glitchtip.sincpro.dev/1
1095
+ ```
1096
+
1097
+ Conf (`sincpro_framework/conf/sincpro_framework_conf.yml`) resolves `sentry_dsn` from `SENTRY_PYTHON_DSN`. If that env is set, `observability_status()["sentry"]` is `on:init`, not `off`. The framework does **not** call `sentry_sdk.init()` and does not reuse the host client.
1098
+
1099
+ Each framework event uses an isolated Sentry `Client` whose `release` is computed once at `UseFramework` init: `{app_name}:{library_version}` (Poetry/installed dist). Not `APP_RELEASE`, not the Python version. Example: `payment-cybersource:5.0.3`. Framework-internal errors use `sincpro-framework:<framework version>`.
1100
+
1101
+ Odoo may capture the same exception with Odoo's release. That second event is intended — two products, two releases, same traceback.
1102
+
1103
+ The bus reports **before** the error handler runs. A handler that swallows an unexpected exception still produces a GlitchTip event. Expected domain errors can be excluded per instance:
1104
+
1105
+ ```python
1106
+ app = UseFramework("payment-cybersource") # release auto-detected from the caller package
1107
+ app.ignore_sentry_exceptions(ValidationError, InsufficientFunds)
1108
+ ```
1109
+
1110
+ Pass `package="sincpro-payments-sdk"` to `UseFramework` when the caller is not the library itself (tests, a thin adapter).
1111
+
1112
+ Observability is optional and must never break the bus. After `build_root_bus()` (or the first `app(dto)` call) every instance exposes a probe:
1113
+
1114
+ ```python
1115
+ status = app.observability_status()
1116
+ status["sentry"] # {active, state, reason} state: off | on | failed
1117
+ status["otel"]
1118
+ ```
1119
+
1120
+ - `off` — extra not installed or conf DSN missing (`sdk_missing`, `dsn_missing`)
1121
+ - `on` — isolated client ready (`init`)
1122
+ - `failed` — DSN present but client construction broke; the bus still runs. Logged as **warning**.
1123
+
1124
+ The instance logger emits: `observability sentry=on:init otel=off:no_endpoint`.
1125
+
1126
+ Do not send traces to GlitchTip (`traces_sample_rate=0`); Tempo stays on OTLP.
1127
+
1074
1128
  ### What OpenTelemetry adds
1075
1129
 
1076
1130
  | Without OTel | With OTel |
@@ -1242,6 +1296,7 @@ where you can define some behavior currently we support the following settings:
1242
1296
 
1243
1297
  - `sincpro_framework_log_level`: Log level for the framework logger. Default: `DEBUG`.
1244
1298
  - `otlp_endpoint`: OTLP exporter endpoint for distributed tracing. Resolved from `OTEL_EXPORTER_OTLP_ENDPOINT` env var. Default: `null` (tracing disabled). Requires `sincpro-framework[opentelemetry]`.
1299
+ - `sentry_dsn`: GlitchTip/Sentry DSN. Resolved from `SENTRY_PYTHON_DSN`. Default: `null` (error reporting disabled). Requires `sentry-sdk` (or `sincpro-framework[sentry]`). The framework uses an isolated client with `release={app_name}:{library_version}` and never calls `sentry_sdk.init()`. Odoo may capture the same error separately. Use `UseFramework.ignore_sentry_exceptions(...)` for expected errors.
1245
1300
 
1246
1301
  Override the config file using another
1247
1302
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sincpro-framework"
3
- version = "3.2.0"
3
+ version = "3.3.0"
4
4
  description = "Sincpro framework to use DDD, Clean architecture, Hexagonal architecture"
5
5
  authors = ["Gutierrez Andres <andru1236@gmail.com>"]
6
6
  readme = "README.md"
@@ -17,11 +17,12 @@ python = "^3.12"
17
17
  dependency-injector = "^4.48.3"
18
18
  pydantic = "^2.12.5"
19
19
  pyyaml = ">=6.0.1"
20
- sincpro-log = "^1.0.1"
20
+ sincpro-log = "^1.1.1"
21
21
  opentelemetry-api = {version = ">=1.20", optional = true}
22
22
  opentelemetry-sdk = {version = ">=1.20", optional = true}
23
23
  opentelemetry-exporter-otlp-proto-grpc = {version = ">=1.20", optional = true}
24
24
  opentelemetry-exporter-otlp-proto-http = {version = ">=1.20", optional = true}
25
+ sentry-sdk = {version = ">=2.0", optional = true}
25
26
 
26
27
  [tool.poetry.extras]
27
28
  opentelemetry = [
@@ -30,6 +31,7 @@ opentelemetry = [
30
31
  "opentelemetry-exporter-otlp-proto-grpc",
31
32
  "opentelemetry-exporter-otlp-proto-http",
32
33
  ]
34
+ sentry = ["sentry-sdk"]
33
35
 
34
36
  [tool.poetry.group.dev.dependencies]
35
37
  isort = "^8.0.0"
@@ -37,9 +39,9 @@ black = "^26.1.0"
37
39
  pytest = "^9.0.2"
38
40
  ipython = "^9.9.0"
39
41
  autoflake = "^2.3.1"
40
- pyright = "^1.1.407"
42
+ pyright = "^1.1.411"
41
43
  jupyterlab = "^4.5.1"
42
- mypy = "^1.19.1"
44
+ mypy = "^2.3.0"
43
45
  mkdocs = ">=1.6.1,<2.0.0"
44
46
  mkdocs-material = "^9.7.1"
45
47
  mkdocstrings = {extras = ["python"], version = "^1.0.0"}
@@ -52,6 +54,7 @@ opentelemetry-api = ">=1.20"
52
54
  opentelemetry-sdk = ">=1.20"
53
55
  opentelemetry-exporter-otlp-proto-grpc = ">=1.20"
54
56
  opentelemetry-exporter-otlp-proto-http = ">=1.20"
57
+ sentry-sdk = ">=2.0"
55
58
 
56
59
 
57
60
  [build-system]
@@ -62,5 +65,7 @@ build-backend = "poetry.core.masonry.api"
62
65
  line-length = 94
63
66
 
64
67
  [tool.pyright]
68
+ venvPath = "."
69
+ venv = ".venv"
65
70
  reportInvalidTypeVarUse = "none"
66
71
  reportInvalidTypeForm = "none"
@@ -1,5 +1,5 @@
1
1
  from logging import Logger
2
- from typing import Callable, Dict, Optional, Type
2
+ from typing import Callable, Dict, Optional, Tuple, Type
3
3
 
4
4
  from .exceptions import DTOAlreadyRegistered, UnknownDTOToExecute
5
5
  from .sincpro_abstractions import (
@@ -11,7 +11,8 @@ from .sincpro_abstractions import (
11
11
  TypeDTOResponse,
12
12
  )
13
13
  from .sincpro_logger import is_logger_in_debug, logger
14
- from .tracing.instrumentation import observe_execution, record_observability_span_error
14
+ from .tracing.instrumentation import observe_execution, record_observability_error
15
+ from .tracing.sentry import record_sentry_error
15
16
 
16
17
 
17
18
  class FeatureBus(Bus):
@@ -22,6 +23,8 @@ class FeatureBus(Bus):
22
23
  self.handle_error: Optional[Callable] = None
23
24
  self.logger: Logger = logger_bus or logger # type: ignore[assignment]
24
25
  self.service_name = ""
26
+ self.sentry_release = ""
27
+ self.ignored_sentry_exceptions: Tuple[Type[Exception], ...] = ()
25
28
 
26
29
  def register_feature(self, dto: Type[DataTransferObject], feature: Feature) -> bool:
27
30
  """Register a feature to the bus"""
@@ -54,7 +57,16 @@ class FeatureBus(Bus):
54
57
  return response
55
58
 
56
59
  except Exception as error:
57
- record_observability_span_error(span, error)
60
+ record_observability_error(
61
+ span,
62
+ error,
63
+ dto_name,
64
+ "feature",
65
+ self.service_name,
66
+ kind="instance",
67
+ release=self.sentry_release,
68
+ ignored_exceptions=self.ignored_sentry_exceptions,
69
+ )
58
70
  if self.handle_error:
59
71
  return self.handle_error(error)
60
72
  raise error
@@ -70,6 +82,8 @@ class ApplicationServiceBus(Bus):
70
82
  self.handle_error: Optional[Callable] = None
71
83
  self.logger = logger_bus or logger
72
84
  self.service_name = ""
85
+ self.sentry_release = ""
86
+ self.ignored_sentry_exceptions: Tuple[Type[Exception], ...] = ()
73
87
 
74
88
  def register_app_service(
75
89
  self, dto: Type[DataTransferObject], app_service: ApplicationService
@@ -108,7 +122,16 @@ class ApplicationServiceBus(Bus):
108
122
  return response
109
123
 
110
124
  except Exception as error:
111
- record_observability_span_error(span, error)
125
+ record_observability_error(
126
+ span,
127
+ error,
128
+ dto_name,
129
+ "application_service",
130
+ self.service_name,
131
+ kind="instance",
132
+ release=self.sentry_release,
133
+ ignored_exceptions=self.ignored_sentry_exceptions,
134
+ )
112
135
  if self.handle_error:
113
136
  return self.handle_error(error)
114
137
  raise error
@@ -138,6 +161,8 @@ class FrameworkBus(Bus):
138
161
  self.app_service_bus = app_service_bus
139
162
  self.handle_error: Optional[Callable] = None
140
163
  self.logger = logger_bus or logger
164
+ self.service_name = ""
165
+ self.sentry_release = ""
141
166
 
142
167
  registered_features = set(self.feature_bus.feature_registry.keys())
143
168
  registered_app_services = set(self.app_service_bus.app_service_registry.keys())
@@ -186,6 +211,15 @@ class FrameworkBus(Bus):
186
211
  )
187
212
 
188
213
  except Exception as error:
214
+ if isinstance(error, (UnknownDTOToExecute, DTOAlreadyRegistered)):
215
+ record_sentry_error(
216
+ error,
217
+ dto_name,
218
+ "framework",
219
+ self.service_name,
220
+ kind="framework",
221
+ release=self.sentry_release,
222
+ )
189
223
  if self.handle_error:
190
224
  return self.handle_error(error)
191
225
 
@@ -1,4 +1,4 @@
1
- from typing import Any, Callable, Dict, Optional, Type, overload
1
+ from typing import Any, Callable, Dict, Optional, Tuple, Type, overload
2
2
 
3
3
  from sincpro_log.logger import LoggerProxy
4
4
 
@@ -23,6 +23,9 @@ class FeatureBus(Bus):
23
23
  feature_registry: Dict[str, Feature]
24
24
  handle_error: Optional[Callable[..., Any]]
25
25
  logger: LoggerProxy
26
+ service_name: str
27
+ sentry_release: str
28
+ ignored_sentry_exceptions: Tuple[Type[Exception], ...]
26
29
 
27
30
  def __init__(self, logger_bus: LoggerProxy = ...) -> None: ...
28
31
  def register_feature(self, dto: Type[DataTransferObject], feature: Feature) -> bool:
@@ -51,6 +54,9 @@ class ApplicationServiceBus(Bus):
51
54
  app_service_registry: Dict[str, ApplicationService]
52
55
  handle_error: Optional[Callable[..., Any]]
53
56
  logger: LoggerProxy
57
+ service_name: str
58
+ sentry_release: str
59
+ ignored_sentry_exceptions: Tuple[Type[Exception], ...]
54
60
 
55
61
  def __init__(self, logger_bus: LoggerProxy = ...) -> None: ...
56
62
  def register_app_service(
@@ -86,6 +92,8 @@ class FrameworkBus(Bus):
86
92
  handle_error: Optional[Callable[..., Any]]
87
93
  logger: LoggerProxy
88
94
  dto_registry: Dict[str, Any]
95
+ service_name: str
96
+ sentry_release: str
89
97
 
90
98
  def __init__(
91
99
  self,
@@ -1,2 +1,3 @@
1
1
  sincpro_framework_log_level: DEBUG
2
2
  otlp_endpoint: $ENV:OTEL_EXPORTER_OTLP_ENDPOINT
3
+ sentry_dsn: $ENV:SENTRY_PYTHON_DSN
@@ -65,6 +65,7 @@ class DefaultFrameworkConfig(SincproConfig):
65
65
 
66
66
  sincpro_framework_log_level: Literal["INFO", "DEBUG"] = "DEBUG"
67
67
  otlp_endpoint: str | None = None
68
+ sentry_dsn: str | None = None
68
69
 
69
70
 
70
71
  def build_config_obj(
@@ -7,9 +7,12 @@ whether opentelemetry is installed or not.
7
7
  Public API:
8
8
  - FrameworkSpanContext — returned by UseFramework.with_trace()
9
9
  - setup_otlp_provider — called by UseFramework.build_root_bus()
10
+ - setup_sentry — called by UseFramework.build_root_bus()
11
+ - UseFramework.observability_status() — probe off/on/failed, never raises
10
12
  """
11
13
 
12
14
  from .provider import setup_otlp_provider
15
+ from .sentry import setup_sentry
13
16
  from .span_context import FrameworkSpanContext
14
17
 
15
- __all__ = ["FrameworkSpanContext", "setup_otlp_provider"]
18
+ __all__ = ["FrameworkSpanContext", "setup_otlp_provider", "setup_sentry"]
@@ -0,0 +1,122 @@
1
+ """Bus-level observability: span creation, log correlation, and error recording."""
2
+
3
+ from contextlib import contextmanager, nullcontext
4
+ from typing import Any, Generator, Tuple, Type
5
+
6
+ from .sentry import SentryKind, record_sentry_error
7
+
8
+ try:
9
+ import opentelemetry # noqa: F401 — existence check only
10
+
11
+ _OTEL_AVAILABLE = True
12
+ except ImportError:
13
+ _OTEL_AVAILABLE = False
14
+
15
+
16
+ @contextmanager
17
+ def observe_execution(
18
+ dto_name: str, layer: str, instance: str, logger: Any
19
+ ) -> Generator[Any, None, None]:
20
+ """Observability context for a single DTO execution on a bus layer.
21
+
22
+ Combines two concerns that must always travel together:
23
+ - An OTel span named after the DTO, tagged with ``sincpro.layer`` and
24
+ ``sincpro.instance`` so spans from different bounded contexts are
25
+ distinguishable in the tracing backend.
26
+ - Logger binding so every log line emitted inside this block carries the
27
+ same ``trace_id`` / ``span_id`` as the exported OTel span.
28
+
29
+ Yields the active span so callers can record errors via
30
+ ``record_span_error(span, error)``.
31
+ """
32
+ with _dto_span(dto_name, layer, instance) as span:
33
+ with _bind_span_to_logger(logger, span):
34
+ yield span
35
+
36
+
37
+ def record_observability_error(
38
+ span: Any,
39
+ error: Exception,
40
+ dto_name: str,
41
+ layer: str,
42
+ instance: str,
43
+ kind: SentryKind = "instance",
44
+ release: str = "",
45
+ ignored_exceptions: Tuple[Type[Exception], ...] = (),
46
+ ) -> None:
47
+ """Record the exception on the OTel span (if any) and in Sentry (if active).
48
+
49
+ Both backends are optional: missing SDK / unset DSN / unset OTLP endpoint
50
+ are silent no-ops. Callers always invoke this; they never check extras.
51
+
52
+ Sentry is invoked before the bus error handler. A handler that swallows
53
+ the error does not hide it from GlitchTip. Expected types in
54
+ ``ignored_exceptions`` skip Sentry only; the span is still marked ERROR.
55
+ """
56
+ try:
57
+ record_observability_span_error(span, error)
58
+ record_sentry_error(
59
+ error,
60
+ dto_name,
61
+ layer,
62
+ instance,
63
+ kind=kind,
64
+ release=release,
65
+ ignored_exceptions=ignored_exceptions,
66
+ )
67
+ except Exception:
68
+ return
69
+
70
+
71
+ def record_observability_span_error(span: Any, error: Exception) -> None:
72
+ """Record an exception on the active span and mark its status as ERROR."""
73
+ try:
74
+ if _OTEL_AVAILABLE and span is not None:
75
+ from opentelemetry.trace import StatusCode
76
+
77
+ span.record_exception(error)
78
+ span.set_status(StatusCode.ERROR, str(error))
79
+ except Exception:
80
+ return
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Private helpers — implementation details of this module only
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ def _dto_span(dto_name: str, layer: str, instance: str):
89
+ """Return an OTel span CM tagged with sincpro attributes, or a no-op."""
90
+ try:
91
+ if _OTEL_AVAILABLE:
92
+ from .provider import get_framework_tracer
93
+
94
+ attrs: dict = {"sincpro.layer": layer}
95
+ if instance:
96
+ attrs["sincpro.instance"] = instance
97
+ tracer = get_framework_tracer("sincpro_framework")
98
+ if tracer is not None:
99
+ return tracer.start_as_current_span(dto_name, attributes=attrs)
100
+ except Exception:
101
+ pass
102
+ return nullcontext()
103
+
104
+
105
+ def _bind_span_to_logger(logger: Any, span: Any):
106
+ """Bind the active OTel span's trace_id/span_id to the shared logger.
107
+
108
+ Works for any case: explicit with_trace(), inherited outer span (FastAPI,
109
+ Celery), or a fresh root span created by the bus itself.
110
+ Returns a context manager that restores the logger's previous fields on exit.
111
+ """
112
+ try:
113
+ if _OTEL_AVAILABLE and span is not None:
114
+ span_ctx = span.get_span_context()
115
+ if span_ctx.is_valid:
116
+ return logger.context(
117
+ trace_id=format(span_ctx.trace_id, "032x"),
118
+ span_id=format(span_ctx.span_id, "016x"),
119
+ )
120
+ except Exception:
121
+ pass
122
+ return nullcontext()
@@ -0,0 +1,116 @@
1
+ """OTLP TracerProvider auto-configuration."""
2
+
3
+ from typing import Any
4
+
5
+ from sincpro_log.logger import LoggerProxy
6
+
7
+ from ..sincpro_conf import settings
8
+ from .status import ComponentStatus, component_status, status_from_exception
9
+
10
+ # Private provider owned by sincpro. Kept separate from the global OTel provider
11
+ # so sincpro spans carry their own service.name even when another framework
12
+ # (Odoo, FastAPI, Celery) already registered the global provider first.
13
+ _sincpro_provider: Any = None
14
+
15
+
16
+ def _get_current_otel_context() -> dict:
17
+ """Return trace_id and span_id from the currently active OTel span, or {}."""
18
+ try:
19
+ from opentelemetry import trace
20
+
21
+ span_ctx = trace.get_current_span().get_span_context()
22
+ if span_ctx.is_valid:
23
+ return {
24
+ "trace_id": format(span_ctx.trace_id, "032x"),
25
+ "span_id": format(span_ctx.span_id, "016x"),
26
+ }
27
+ except Exception:
28
+ pass
29
+ return {}
30
+
31
+
32
+ def _host_provider_is_real() -> bool:
33
+ try:
34
+ from opentelemetry import trace
35
+
36
+ current_type = type(trace.get_tracer_provider()).__name__
37
+ return current_type not in ("ProxyTracerProvider", "NoOpTracerProvider")
38
+ except Exception:
39
+ return False
40
+
41
+
42
+ def setup_otlp_provider(
43
+ service_name: str, logger: LoggerProxy | None = None
44
+ ) -> ComponentStatus:
45
+ """Configure a dedicated TracerProvider for this bounded context.
46
+
47
+ Never raises. Missing SDK or endpoint is ``off``. A host that already
48
+ registered a real TracerProvider is ``on:host`` even without our endpoint.
49
+ """
50
+ global _sincpro_provider
51
+
52
+ try:
53
+ from opentelemetry import trace
54
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
55
+ from opentelemetry.sdk.trace import TracerProvider
56
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
57
+ from opentelemetry.sdk.trace.sampling import ALWAYS_ON, ParentBased
58
+ except ImportError:
59
+ return component_status(False, "off", "sdk_missing")
60
+ except Exception as exc:
61
+ return status_from_exception(exc)
62
+
63
+ endpoint: str | None = settings.otlp_endpoint
64
+ if not endpoint:
65
+ if _host_provider_is_real():
66
+ return component_status(True, "on", "host")
67
+ return component_status(False, "off", "no_endpoint")
68
+
69
+ try:
70
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
71
+ except ImportError:
72
+ try:
73
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
74
+ except ImportError:
75
+ return component_status(False, "failed", "exporter_missing")
76
+ except Exception as exc:
77
+ return status_from_exception(exc)
78
+ except Exception as exc:
79
+ return status_from_exception(exc)
80
+
81
+ try:
82
+ provider = TracerProvider(
83
+ resource=Resource(attributes={SERVICE_NAME: service_name}),
84
+ sampler=ParentBased(root=ALWAYS_ON),
85
+ )
86
+ provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
87
+ _sincpro_provider = provider
88
+
89
+ current_type: str = type(trace.get_tracer_provider()).__name__
90
+ if current_type in ("ProxyTracerProvider", "NoOpTracerProvider"):
91
+ trace.set_tracer_provider(provider)
92
+
93
+ if logger is not None:
94
+ logger.set_getter_context(_get_current_otel_context)
95
+ return component_status(True, "on", "init")
96
+ except Exception as exc:
97
+ return status_from_exception(exc)
98
+
99
+
100
+ def get_framework_tracer(instrumentation_name: str) -> Any:
101
+ """Return a Tracer from sincpro's private provider, or the global fallback.
102
+
103
+ Using the private provider (when setup_otlp_provider was called) ensures spans
104
+ carry sincpro's own service.name rather than inheriting the host application's.
105
+ Falls back to the global provider when running without explicit OTel setup —
106
+ covers the case where the host already configured a provider that sincpro should
107
+ just piggyback on.
108
+ """
109
+ try:
110
+ if _sincpro_provider is not None:
111
+ return _sincpro_provider.get_tracer(instrumentation_name)
112
+ from opentelemetry import trace as otel_trace
113
+
114
+ return otel_trace.get_tracer(instrumentation_name)
115
+ except Exception:
116
+ return None