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.
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/PKG-INFO +62 -5
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/README.md +58 -3
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/pyproject.toml +9 -4
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/bus.py +38 -4
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/bus.pyi +9 -1
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/conf/sincpro_framework_conf.yml +1 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_conf.py +1 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/tracing/__init__.py +4 -1
- sincpro_framework-3.3.0/sincpro_framework/tracing/instrumentation.py +122 -0
- sincpro_framework-3.3.0/sincpro_framework/tracing/provider.py +116 -0
- sincpro_framework-3.3.0/sincpro_framework/tracing/sentry.py +234 -0
- sincpro_framework-3.3.0/sincpro_framework/tracing/status.py +35 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/use_bus.py +125 -9
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/use_bus.pyi +15 -1
- sincpro_framework-3.2.0/sincpro_framework/tracing/instrumentation.py +0 -77
- sincpro_framework-3.2.0/sincpro_framework/tracing/provider.py +0 -91
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/LICENSE.md +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/__init__.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/__init__.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/framework_context.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/framework_context.pyi +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/context/mixin.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/ddd/__init__.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/ddd/value_object.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/error_handler.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/exceptions.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/__init__.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/domain/__init__.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/domain/extractor.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/domain/models.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/__init__.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/framework_docs_extractor.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/json_schema_generator.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/mkdocs_markdown_generator.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/mkdocs_yaml_generator.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/sincpro_introspector.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/infrastructure/static_site_generator.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/service.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/generate_documentation/sincpro_framework_ai_guide.json +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/ioc.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/middleware.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/py.typed +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_abstractions.py +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_abstractions.pyi +0 -0
- {sincpro_framework-3.2.0 → sincpro_framework-3.3.0}/sincpro_framework/sincpro_logger.py +0 -0
- {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.
|
|
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:
|
|
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
|
|
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
|
|
1079
|
+
## Observability
|
|
1080
|
+
|
|
1081
|
+
The bus always instruments. Extras and env vars only decide **where** data goes.
|
|
1072
1082
|
|
|
1073
|
-
|
|
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
|
|
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
|
|
1052
|
+
## Observability
|
|
1053
|
+
|
|
1054
|
+
The bus always instruments. Extras and env vars only decide **where** data goes.
|
|
1047
1055
|
|
|
1048
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
+
pyright = "^1.1.411"
|
|
41
43
|
jupyterlab = "^4.5.1"
|
|
42
|
-
mypy = "^
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|