quapp-common 0.0.13.dev9__tar.gz → 0.0.14.dev1__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.
- {quapp_common-0.0.13.dev9/quapp_common.egg-info → quapp_common-0.0.14.dev1}/PKG-INFO +64 -2
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/README.md +63 -1
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/pyproject.toml +1 -1
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/post_processing_task.py +3 -1
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/invocation.py +24 -9
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_fetcher.py +5 -2
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_fetching.py +5 -2
- quapp_common-0.0.14.dev1/quapp_common/config/log_context.py +186 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/config/logging_config.py +33 -5
- quapp_common-0.0.14.dev1/quapp_common/config/thread_config.py +28 -0
- quapp_common-0.0.14.dev1/quapp_common/data/response/error_report.py +191 -0
- quapp_common-0.0.14.dev1/quapp_common/enum/error_category.py +91 -0
- quapp_common-0.0.14.dev1/quapp_common/enum/runtime_phase.py +118 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/device/custom_device.py +4 -1
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/device/device.py +48 -29
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/invocation.py +13 -1
- quapp_common-0.0.14.dev1/quapp_common/util/error_classifier.py +456 -0
- quapp_common-0.0.14.dev1/quapp_common/util/invocation_failure.py +103 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/util/response_utils.py +21 -6
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1/quapp_common.egg-info}/PKG-INFO +64 -2
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/SOURCES.txt +8 -0
- quapp_common-0.0.14.dev1/tests/test_error_classifier.py +382 -0
- quapp_common-0.0.14.dev1/tests/test_error_report.py +405 -0
- quapp_common-0.0.14.dev1/tests/test_invocation_failure.py +148 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_logging_config.py +35 -0
- quapp_common-0.0.13.dev9/quapp_common/config/log_context.py +0 -102
- quapp_common-0.0.13.dev9/quapp_common/config/thread_config.py +0 -10
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/LICENSE +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/async_invocation_task.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/bridge.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/dispatcher.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/setup.cfg +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_dispatcher_runner_error.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/tests/test_result_serializer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quapp-common
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.14.dev1
|
|
4
4
|
Summary: Quapp common library supporting Quapp Platform for Quantum Computing
|
|
5
5
|
Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -114,7 +114,69 @@ Notes:
|
|
|
114
114
|
|
|
115
115
|
## Recently Changes Highlights
|
|
116
116
|
|
|
117
|
-
### v0.0.14.dev1 — Error
|
|
117
|
+
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
118
|
+
|
|
119
|
+
Tiếp nối Phase 0. Bổ sung phân loại lỗi có ngữ nghĩa và envelope dùng chung cho
|
|
120
|
+
Function Log lẫn Job Log. **Additive**: `job_result` chỉ thêm field `error`,
|
|
121
|
+
`message` và `exception` giữ nguyên; contract callback URL không đổi.
|
|
122
|
+
|
|
123
|
+
**Taxonomy — hai chiều phân loại độc lập**
|
|
124
|
+
- `enum/error_category.py`: `ErrorCategory` 13 giá trị, nhóm theo *ai xử lý
|
|
125
|
+
được* (`owner_of()` → USER / WORKSPACE_ADMIN / PROVIDER / PLATFORM).
|
|
126
|
+
- `enum/runtime_phase.py`: `RuntimePhase` 9 giá trị. Tách được
|
|
127
|
+
`PROVIDER_INVOCATION` / `COMPILATION` / `EXECUTION` — trước đây auth sai,
|
|
128
|
+
transpile fail và provider từ chối job đều ra chung nhãn `EXECUTION`.
|
|
129
|
+
`PHASE_TO_STEP` giữ nguyên contract `InvocationStep` của callback URL.
|
|
130
|
+
|
|
131
|
+
**Classifier (`util/error_classifier.py`)** — tổ hợp 5 tín hiệu theo độ tin cậy:
|
|
132
|
+
1. `phase` — biết chắc từ context manager, không suy đoán;
|
|
133
|
+
2. `frame_owner()` — code của ai, dựa vào đường dẫn frame sâu nhất
|
|
134
|
+
(`function/` = user, `quapp_*` = platform, `site-packages/` = SDK);
|
|
135
|
+
3. tên class exception, đi hết **cause chain** qua cả `__cause__` *và*
|
|
136
|
+
`__context__` — cần thiết vì nhiều provider lib bọc lỗi bằng
|
|
137
|
+
`raise ValueError(...)` trong khối `except` mà không có `from`;
|
|
138
|
+
4. attribute đọc được: HTTP status, AWS error code, exit code, signal;
|
|
139
|
+
5. message pattern (dùng cuối cùng).
|
|
140
|
+
|
|
141
|
+
`register_rules()` cho phép lib provider đăng ký rule đặc thù SDK khi import,
|
|
142
|
+
nên `qapp-common` không phải import qiskit/braket. Batch đăng ký sau được xét
|
|
143
|
+
trước; trong cùng batch giữ nguyên thứ tự. Rule ném lỗi bị bỏ qua chứ không làm
|
|
144
|
+
hỏng đường báo lỗi. 20 rule generic sẵn có.
|
|
145
|
+
|
|
146
|
+
Fallback nhiều tầng nên **không bao giờ trả `UNKNOWN` một cách mù**: không rule
|
|
147
|
+
nào khớp thì suy từ frame ownership, rồi tới phase.
|
|
148
|
+
|
|
149
|
+
**Envelope (`data/response/error_report.py`)** — `job_result['error']` gồm
|
|
150
|
+
`code`, `category`, `phase`, `owner`, `retryable`, `rootCause` (type + detail +
|
|
151
|
+
file:line), `causeChain`, `stackTrace` (full, đã redact),
|
|
152
|
+
`handlerStackTrace` (stack JS/.NET cho handler không phải Python),
|
|
153
|
+
`context`, `resolution` (summary + steps + docUrl), `retry`, `traceId`.
|
|
154
|
+
`log_fields()` bind cùng vocabulary vào Function Log.
|
|
155
|
+
|
|
156
|
+
**Timeline (`config/log_context.py`)** — context manager `phase()` phát
|
|
157
|
+
`phase_start` / `phase_end` / `phase_failed` kèm `duration_ms`. Đây là dữ liệu
|
|
158
|
+
để UI vẽ timeline và chỉ ra job chết ở bước nào. `phase_failed` ở mức WARNING để
|
|
159
|
+
thấy được khi chạy `LOG_LEVEL=INFO`.
|
|
160
|
+
|
|
161
|
+
`_FAILED_PHASE` ghi lại phase mà exception thoát ra: khối `except` xử lý lỗi gần
|
|
162
|
+
như luôn nằm **ngoài** `with phase(...)`, lúc đó phase đang mở đã bị reset trong
|
|
163
|
+
`finally`. Không có nó thì mọi `ErrorReport` đều `phase=None` và fallback theo
|
|
164
|
+
phase không bao giờ chạy. Dùng `effective_phase()` ở đường báo lỗi.
|
|
165
|
+
|
|
166
|
+
**`config/thread_config.py`** — `ContextAwareThreadPoolExecutor` copy contextvars
|
|
167
|
+
khi submit. `ThreadPoolExecutor` thuần không làm việc này (khác
|
|
168
|
+
`asyncio.to_thread`), nên analysis + finalization chạy trong
|
|
169
|
+
`circuit_running_pool` sẽ mất `job_id` / `trace_id` / `phase`.
|
|
170
|
+
|
|
171
|
+
**Call-site** — `phase()` bọc PREPROCESSING / DEVICE_SELECTION /
|
|
172
|
+
PROVIDER_INVOCATION / COMPILATION / EXECUTION / ANALYSIS; toàn bộ 13 call-site
|
|
173
|
+
của `build_error_job_response()` truyền `phase=`.
|
|
174
|
+
|
|
175
|
+
**Test** — `tests/test_error_classifier.py` (52 case: cause chain, HTTP/AWS
|
|
176
|
+
code, frame ownership, phase fallback, rule ordering) và
|
|
177
|
+
`tests/test_error_report.py` (44 case: envelope, timeline, thread pool).
|
|
178
|
+
|
|
179
|
+
### v0.0.13.dev9 — Error logging & observability (Phase 0)
|
|
118
180
|
|
|
119
181
|
Mục tiêu: Function Log và Job Log phải chỉ ra được nguyên nhân lỗi. Toàn bộ thay
|
|
120
182
|
đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
|
|
@@ -73,7 +73,69 @@ Notes:
|
|
|
73
73
|
|
|
74
74
|
## Recently Changes Highlights
|
|
75
75
|
|
|
76
|
-
### v0.0.14.dev1 — Error
|
|
76
|
+
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
77
|
+
|
|
78
|
+
Tiếp nối Phase 0. Bổ sung phân loại lỗi có ngữ nghĩa và envelope dùng chung cho
|
|
79
|
+
Function Log lẫn Job Log. **Additive**: `job_result` chỉ thêm field `error`,
|
|
80
|
+
`message` và `exception` giữ nguyên; contract callback URL không đổi.
|
|
81
|
+
|
|
82
|
+
**Taxonomy — hai chiều phân loại độc lập**
|
|
83
|
+
- `enum/error_category.py`: `ErrorCategory` 13 giá trị, nhóm theo *ai xử lý
|
|
84
|
+
được* (`owner_of()` → USER / WORKSPACE_ADMIN / PROVIDER / PLATFORM).
|
|
85
|
+
- `enum/runtime_phase.py`: `RuntimePhase` 9 giá trị. Tách được
|
|
86
|
+
`PROVIDER_INVOCATION` / `COMPILATION` / `EXECUTION` — trước đây auth sai,
|
|
87
|
+
transpile fail và provider từ chối job đều ra chung nhãn `EXECUTION`.
|
|
88
|
+
`PHASE_TO_STEP` giữ nguyên contract `InvocationStep` của callback URL.
|
|
89
|
+
|
|
90
|
+
**Classifier (`util/error_classifier.py`)** — tổ hợp 5 tín hiệu theo độ tin cậy:
|
|
91
|
+
1. `phase` — biết chắc từ context manager, không suy đoán;
|
|
92
|
+
2. `frame_owner()` — code của ai, dựa vào đường dẫn frame sâu nhất
|
|
93
|
+
(`function/` = user, `quapp_*` = platform, `site-packages/` = SDK);
|
|
94
|
+
3. tên class exception, đi hết **cause chain** qua cả `__cause__` *và*
|
|
95
|
+
`__context__` — cần thiết vì nhiều provider lib bọc lỗi bằng
|
|
96
|
+
`raise ValueError(...)` trong khối `except` mà không có `from`;
|
|
97
|
+
4. attribute đọc được: HTTP status, AWS error code, exit code, signal;
|
|
98
|
+
5. message pattern (dùng cuối cùng).
|
|
99
|
+
|
|
100
|
+
`register_rules()` cho phép lib provider đăng ký rule đặc thù SDK khi import,
|
|
101
|
+
nên `qapp-common` không phải import qiskit/braket. Batch đăng ký sau được xét
|
|
102
|
+
trước; trong cùng batch giữ nguyên thứ tự. Rule ném lỗi bị bỏ qua chứ không làm
|
|
103
|
+
hỏng đường báo lỗi. 20 rule generic sẵn có.
|
|
104
|
+
|
|
105
|
+
Fallback nhiều tầng nên **không bao giờ trả `UNKNOWN` một cách mù**: không rule
|
|
106
|
+
nào khớp thì suy từ frame ownership, rồi tới phase.
|
|
107
|
+
|
|
108
|
+
**Envelope (`data/response/error_report.py`)** — `job_result['error']` gồm
|
|
109
|
+
`code`, `category`, `phase`, `owner`, `retryable`, `rootCause` (type + detail +
|
|
110
|
+
file:line), `causeChain`, `stackTrace` (full, đã redact),
|
|
111
|
+
`handlerStackTrace` (stack JS/.NET cho handler không phải Python),
|
|
112
|
+
`context`, `resolution` (summary + steps + docUrl), `retry`, `traceId`.
|
|
113
|
+
`log_fields()` bind cùng vocabulary vào Function Log.
|
|
114
|
+
|
|
115
|
+
**Timeline (`config/log_context.py`)** — context manager `phase()` phát
|
|
116
|
+
`phase_start` / `phase_end` / `phase_failed` kèm `duration_ms`. Đây là dữ liệu
|
|
117
|
+
để UI vẽ timeline và chỉ ra job chết ở bước nào. `phase_failed` ở mức WARNING để
|
|
118
|
+
thấy được khi chạy `LOG_LEVEL=INFO`.
|
|
119
|
+
|
|
120
|
+
`_FAILED_PHASE` ghi lại phase mà exception thoát ra: khối `except` xử lý lỗi gần
|
|
121
|
+
như luôn nằm **ngoài** `with phase(...)`, lúc đó phase đang mở đã bị reset trong
|
|
122
|
+
`finally`. Không có nó thì mọi `ErrorReport` đều `phase=None` và fallback theo
|
|
123
|
+
phase không bao giờ chạy. Dùng `effective_phase()` ở đường báo lỗi.
|
|
124
|
+
|
|
125
|
+
**`config/thread_config.py`** — `ContextAwareThreadPoolExecutor` copy contextvars
|
|
126
|
+
khi submit. `ThreadPoolExecutor` thuần không làm việc này (khác
|
|
127
|
+
`asyncio.to_thread`), nên analysis + finalization chạy trong
|
|
128
|
+
`circuit_running_pool` sẽ mất `job_id` / `trace_id` / `phase`.
|
|
129
|
+
|
|
130
|
+
**Call-site** — `phase()` bọc PREPROCESSING / DEVICE_SELECTION /
|
|
131
|
+
PROVIDER_INVOCATION / COMPILATION / EXECUTION / ANALYSIS; toàn bộ 13 call-site
|
|
132
|
+
của `build_error_job_response()` truyền `phase=`.
|
|
133
|
+
|
|
134
|
+
**Test** — `tests/test_error_classifier.py` (52 case: cause chain, HTTP/AWS
|
|
135
|
+
code, frame ownership, phase fallback, rule ordering) và
|
|
136
|
+
`tests/test_error_report.py` (44 case: envelope, timeline, thread pool).
|
|
137
|
+
|
|
138
|
+
### v0.0.13.dev9 — Error logging & observability (Phase 0)
|
|
77
139
|
|
|
78
140
|
Mục tiêu: Function Log và Job Log phải chỉ ra được nguyên nhân lỗi. Toàn bộ thay
|
|
79
141
|
đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quapp-common"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.14.dev1"
|
|
8
8
|
description = "Quapp common library supporting Quapp Platform for Quantum Computing"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
|
|
@@ -5,6 +5,7 @@ from .async_task import AsyncTask
|
|
|
5
5
|
from ..config.logging_config import job_logger
|
|
6
6
|
from ..data.promise.post_processing_promise import PostProcessingPromise
|
|
7
7
|
from ..enum.media_type import MediaType
|
|
8
|
+
from ..enum.runtime_phase import RuntimePhase
|
|
8
9
|
from ..util.http_utils import get_job_id_from_url
|
|
9
10
|
from ..util.json_parser_utils import parse
|
|
10
11
|
|
|
@@ -50,7 +51,8 @@ class PostProcessingTask(AsyncTask):
|
|
|
50
51
|
job_response = build_error_job_response(
|
|
51
52
|
exception, job_response,
|
|
52
53
|
message='Error when post processing job result',
|
|
53
|
-
stage='while post-processing the job result'
|
|
54
|
+
stage='while post-processing the job result',
|
|
55
|
+
phase=RuntimePhase.POST_PROCESSING)
|
|
54
56
|
|
|
55
57
|
update_job_metadata(job_response,
|
|
56
58
|
self.promise.callback_url.on_error)
|
{quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/invocation.py
RENAMED
|
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
|
|
7
7
|
from ..callback.update_job_metadata import update_job_metadata
|
|
8
8
|
from ...component.device.device_selection import DeviceSelection
|
|
9
|
+
from ...config.log_context import bind_job, enrich_job_context, phase
|
|
9
10
|
from ...config.logging_config import job_logger
|
|
10
11
|
from ...config.thread_config import circuit_running_pool
|
|
11
12
|
from ...data.backend.backend_information import BackendInformation
|
|
@@ -15,6 +16,7 @@ from ...data.response.authentication import Authentication
|
|
|
15
16
|
from ...data.response.custom_header import CustomHeader
|
|
16
17
|
from ...data.response.job_response import JobResponse
|
|
17
18
|
from ...enum.invocation_step import InvocationStep
|
|
19
|
+
from ...enum.runtime_phase import RuntimePhase
|
|
18
20
|
from ...enum.sdk import Sdk
|
|
19
21
|
from ...enum.status.status_code import StatusCode
|
|
20
22
|
from ...model.provider.provider import Provider
|
|
@@ -82,8 +84,12 @@ class Invocation(ABC):
|
|
|
82
84
|
return None
|
|
83
85
|
|
|
84
86
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
with phase(RuntimePhase.DEVICE_SELECTION):
|
|
88
|
+
self.__prepare_backend_data(circuit)
|
|
89
|
+
|
|
90
|
+
enrich_job_context(
|
|
91
|
+
device=self.backend_information.device_name,
|
|
92
|
+
provider=self.backend_information.provider_tag.value)
|
|
87
93
|
self.logger.info(
|
|
88
94
|
f"Backend data prepared: device {self.backend_information.device_name}, provider {self.backend_information.provider_tag.value}")
|
|
89
95
|
|
|
@@ -94,7 +100,8 @@ class Invocation(ABC):
|
|
|
94
100
|
|
|
95
101
|
job_response = build_error_job_response(exception,
|
|
96
102
|
message='Error when prepare backend data',
|
|
97
|
-
stage='while preparing the backend'
|
|
103
|
+
stage='while preparing the backend',
|
|
104
|
+
phase=RuntimePhase.DEVICE_SELECTION)
|
|
98
105
|
job_response.authentication = self.authentication
|
|
99
106
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
100
107
|
InvocationStep.PREPARATION).on_error)
|
|
@@ -125,10 +132,15 @@ class Invocation(ABC):
|
|
|
125
132
|
self.logger.debug(
|
|
126
133
|
f"Executing job with provider tag: {provider_tag.value}")
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
# PROVIDER_INVOCATION tách riêng khỏi EXECUTION: lỗi authenticate
|
|
136
|
+
# với provider và lỗi provider từ chối circuit là hai chuyện khác
|
|
137
|
+
# nhau, trước đây cùng ra nhãn EXECUTION.
|
|
138
|
+
with phase(RuntimePhase.PROVIDER_INVOCATION):
|
|
139
|
+
provider = self._create_provider()
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
self.logger.debug(
|
|
142
|
+
f"Executing job with device name: {device_name}")
|
|
143
|
+
device = self._create_device(provider)
|
|
132
144
|
|
|
133
145
|
except Exception as exception:
|
|
134
146
|
self.logger.exception(
|
|
@@ -137,7 +149,8 @@ class Invocation(ABC):
|
|
|
137
149
|
|
|
138
150
|
job_response = build_error_job_response(exception,
|
|
139
151
|
message='Error when create provider or device',
|
|
140
|
-
stage='while running the circuit on the device'
|
|
152
|
+
stage='while running the circuit on the device',
|
|
153
|
+
phase=RuntimePhase.PROVIDER_INVOCATION)
|
|
141
154
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
142
155
|
InvocationStep.EXECUTION).on_error, )
|
|
143
156
|
|
|
@@ -170,7 +183,8 @@ class Invocation(ABC):
|
|
|
170
183
|
InvocationStep.PREPARATION).on_start)
|
|
171
184
|
|
|
172
185
|
try:
|
|
173
|
-
|
|
186
|
+
with phase(RuntimePhase.PREPROCESSING):
|
|
187
|
+
circuit = circuit_preparation_fn(self.input)
|
|
174
188
|
|
|
175
189
|
if circuit is None:
|
|
176
190
|
self.logger.warning(
|
|
@@ -190,7 +204,8 @@ class Invocation(ABC):
|
|
|
190
204
|
|
|
191
205
|
job_response = build_error_job_response(exception, job_response,
|
|
192
206
|
message='Error when prepare circuit',
|
|
193
|
-
stage='while preparing the circuit'
|
|
207
|
+
stage='while preparing the circuit',
|
|
208
|
+
phase=RuntimePhase.PREPROCESSING)
|
|
194
209
|
|
|
195
210
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
196
211
|
InvocationStep.PREPARATION).on_error)
|
{quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_fetcher.py
RENAMED
|
@@ -16,6 +16,7 @@ from ...data.response.custom_header import CustomHeader
|
|
|
16
16
|
from ...data.response.job_response import JobResponse
|
|
17
17
|
from ...enum.invocation_step import InvocationStep
|
|
18
18
|
from ...enum.media_type import MediaType
|
|
19
|
+
from ...enum.runtime_phase import RuntimePhase
|
|
19
20
|
from ...enum.status.job_status import JobStatus
|
|
20
21
|
from ...enum.status.status_code import StatusCode
|
|
21
22
|
from ...util.json_parser_utils import parse
|
|
@@ -135,7 +136,8 @@ class JobFetcher(ABC):
|
|
|
135
136
|
exception, job_response,
|
|
136
137
|
message='Error when fetching job with provider_job_id {0}'.format(
|
|
137
138
|
self.provider_job_id),
|
|
138
|
-
stage='while fetching the job result from the provider'
|
|
139
|
+
stage='while fetching the job result from the provider',
|
|
140
|
+
phase=RuntimePhase.POLLING)
|
|
139
141
|
update_job_metadata(job_response, self.callback_urls[
|
|
140
142
|
InvocationStep.EXECUTION].on_error)
|
|
141
143
|
|
|
@@ -213,7 +215,8 @@ class JobFetcher(ABC):
|
|
|
213
215
|
job_response = build_error_job_response(
|
|
214
216
|
exception, job_response,
|
|
215
217
|
message='Error when analyzing job result',
|
|
216
|
-
stage='while analysing the job result'
|
|
218
|
+
stage='while analysing the job result',
|
|
219
|
+
phase=RuntimePhase.ANALYSIS)
|
|
217
220
|
|
|
218
221
|
update_job_metadata(job_response, callback_url.on_error)
|
|
219
222
|
return None
|
{quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_fetching.py
RENAMED
|
@@ -17,6 +17,7 @@ from ...data.response.custom_header import CustomHeader
|
|
|
17
17
|
from ...data.response.job_response import JobResponse
|
|
18
18
|
from ...enum.invocation_step import InvocationStep
|
|
19
19
|
from ...enum.media_type import MediaType
|
|
20
|
+
from ...enum.runtime_phase import RuntimePhase
|
|
20
21
|
from ...enum.status.job_status import JobStatus
|
|
21
22
|
from ...enum.status.status_code import StatusCode
|
|
22
23
|
from ...util.json_parser_utils import parse
|
|
@@ -97,7 +98,8 @@ class JobFetching(ABC):
|
|
|
97
98
|
exception, job_response,
|
|
98
99
|
message='Exception when fetch job with provider_job_id {0}'.format(
|
|
99
100
|
self.provider_job_id),
|
|
100
|
-
stage='while fetching the job result from the provider'
|
|
101
|
+
stage='while fetching the job result from the provider',
|
|
102
|
+
phase=RuntimePhase.POLLING)
|
|
101
103
|
|
|
102
104
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
103
105
|
InvocationStep.EXECUTION).on_error)
|
|
@@ -162,7 +164,8 @@ class JobFetching(ABC):
|
|
|
162
164
|
exception, job_response,
|
|
163
165
|
message='Exception when analyst job result with provider_job_id {0}'.format(
|
|
164
166
|
self.provider_job_id),
|
|
165
|
-
stage='while analysing the job result'
|
|
167
|
+
stage='while analysing the job result',
|
|
168
|
+
phase=RuntimePhase.ANALYSIS)
|
|
166
169
|
|
|
167
170
|
update_job_metadata(job_response, callback_url.on_error)
|
|
168
171
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# log_context.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""Per-job logging context.
|
|
6
|
+
|
|
7
|
+
Vì sao cần: hiện tại mỗi nơi phải tự truyền ``job_id`` vào ``job_logger()``, và
|
|
8
|
+
rất nhiều nơi quên -- ``Device.__init__``, ``Provider``, ``DeviceSelection``,
|
|
9
|
+
các ``*Factory`` đều dùng ``logger.bind(context='...')`` hoặc gọi
|
|
10
|
+
``job_logger()`` không tham số, nên log của chúng rơi vào context
|
|
11
|
+
``"QuappLibs"`` và không gắn được vào job nào trong Job Log UI.
|
|
12
|
+
|
|
13
|
+
Cách xử lý: bind context MỘT lần ở đầu request vào ``contextvars``. Mọi log sau
|
|
14
|
+
đó -- kể cả log của thư viện bên thứ ba đi qua ``InterceptHandler`` -- tự có đủ
|
|
15
|
+
field ``job_id`` / ``trace_id`` / ``sdk`` / ``provider`` / ``device``.
|
|
16
|
+
|
|
17
|
+
Lưu ý về thread: ``contextvars`` KHÔNG tự propagate qua
|
|
18
|
+
``ThreadPoolExecutor.submit()``. ``circuit_running_pool`` được dùng cho analysis
|
|
19
|
+
và finalization, nên hai phase đó sẽ mất context nếu pool không copy context --
|
|
20
|
+
xem ``thread_config.ContextAwareThreadPoolExecutor`` (Phase 1).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import contextvars
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
|
|
28
|
+
from loguru import logger
|
|
29
|
+
|
|
30
|
+
from ..enum.runtime_phase import RuntimePhase
|
|
31
|
+
|
|
32
|
+
JOB_ID = 'job_id'
|
|
33
|
+
TRACE_ID = 'trace_id'
|
|
34
|
+
|
|
35
|
+
_CONTEXT_KEYS = (JOB_ID, TRACE_ID, 'sdk', 'provider', 'device',
|
|
36
|
+
'provider_job_id')
|
|
37
|
+
|
|
38
|
+
_JOB_CONTEXT: contextvars.ContextVar[dict] = contextvars.ContextVar(
|
|
39
|
+
'quapp_job_context', default={})
|
|
40
|
+
|
|
41
|
+
_PHASE: contextvars.ContextVar['RuntimePhase | None'] = contextvars.ContextVar(
|
|
42
|
+
'quapp_phase', default=None)
|
|
43
|
+
|
|
44
|
+
# Phase mà exception thoát ra gần nhất.
|
|
45
|
+
#
|
|
46
|
+
# Cần biến riêng vì `phase()` reset _PHASE trong `finally`, mà khối `except` xử
|
|
47
|
+
# lý lỗi hầu như luôn nằm NGOÀI khối `with phase(...)`:
|
|
48
|
+
#
|
|
49
|
+
# try:
|
|
50
|
+
# with phase(RuntimePhase.COMPILATION):
|
|
51
|
+
# transpile(...)
|
|
52
|
+
# except Exception: # <- _PHASE đã là None ở đây
|
|
53
|
+
# build_error_job_response(...)
|
|
54
|
+
#
|
|
55
|
+
# Không có biến này thì mọi ErrorReport đều có phase=None và fallback theo phase
|
|
56
|
+
# không bao giờ chạy.
|
|
57
|
+
_FAILED_PHASE: contextvars.ContextVar[
|
|
58
|
+
'RuntimePhase | None'] = contextvars.ContextVar(
|
|
59
|
+
'quapp_failed_phase', default=None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bind_job(job_id: str = None, *, sdk=None, provider=None, device=None,
|
|
63
|
+
provider_job_id=None, trace_id=None):
|
|
64
|
+
"""Khởi tạo context cho một job và trả về logger đã bind.
|
|
65
|
+
|
|
66
|
+
Gọi một lần càng sớm càng tốt -- ``Handler.__init__`` hoặc ``index.py``.
|
|
67
|
+
|
|
68
|
+
@param job_id: id job phía Quapp backend
|
|
69
|
+
@param trace_id: tự sinh nếu không truyền; dùng để join Function Log <-> Job Log
|
|
70
|
+
@return: logger đã bind toàn bộ context
|
|
71
|
+
"""
|
|
72
|
+
context = {JOB_ID : job_id,
|
|
73
|
+
TRACE_ID : trace_id or uuid.uuid4().hex[:16],
|
|
74
|
+
'sdk' : sdk,
|
|
75
|
+
'provider' : provider,
|
|
76
|
+
'device' : device,
|
|
77
|
+
'provider_job_id': provider_job_id}
|
|
78
|
+
_JOB_CONTEXT.set(context)
|
|
79
|
+
_FAILED_PHASE.set(None)
|
|
80
|
+
return logger.bind(**context)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def enrich_job_context(**fields):
|
|
84
|
+
"""Bổ sung field khi biết thêm thông tin (device_name sau device selection,
|
|
85
|
+
provider_job_id sau khi submit...). Chỉ nhận các key trong _CONTEXT_KEYS.
|
|
86
|
+
|
|
87
|
+
@return: logger đã bind context mới
|
|
88
|
+
"""
|
|
89
|
+
context = dict(_JOB_CONTEXT.get())
|
|
90
|
+
context.update(
|
|
91
|
+
{key: value for key, value in fields.items() if key in _CONTEXT_KEYS})
|
|
92
|
+
_JOB_CONTEXT.set(context)
|
|
93
|
+
return logger.bind(**context)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def job_context() -> dict:
|
|
97
|
+
"""Snapshot context hiện tại. Trả về dict rỗng nếu chưa bind."""
|
|
98
|
+
return dict(_JOB_CONTEXT.get())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def current_job_id() -> str | None:
|
|
102
|
+
return _JOB_CONTEXT.get().get(JOB_ID)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def trace_id() -> str | None:
|
|
106
|
+
return _JOB_CONTEXT.get().get(TRACE_ID)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def current_phase():
|
|
110
|
+
"""RuntimePhase đang mở, hoặc None."""
|
|
111
|
+
return _PHASE.get()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def failed_phase():
|
|
115
|
+
"""Phase mà exception thoát ra gần nhất trong job này, hoặc None."""
|
|
116
|
+
return _FAILED_PHASE.get()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def effective_phase():
|
|
120
|
+
"""Phase dùng để phân loại lỗi: phase đang mở, nếu không thì phase vừa fail.
|
|
121
|
+
|
|
122
|
+
Đây là hàm mà đường báo lỗi nên gọi -- khối `except` gần như luôn nằm ngoài
|
|
123
|
+
`with phase(...)`, nên `current_phase()` một mình sẽ trả None.
|
|
124
|
+
"""
|
|
125
|
+
return _PHASE.get() or _FAILED_PHASE.get()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def current_phase_value():
|
|
129
|
+
"""Giá trị str của phase đang mở, để bind vào log."""
|
|
130
|
+
phase = _PHASE.get()
|
|
131
|
+
return phase.value if phase is not None else None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@contextmanager
|
|
135
|
+
def phase(runtime_phase: RuntimePhase):
|
|
136
|
+
"""Đánh dấu một runtime phase.
|
|
137
|
+
|
|
138
|
+
Làm ba việc cùng lúc:
|
|
139
|
+
|
|
140
|
+
1. gán ``phase`` cho mọi log bên trong, kể cả log của thư viện bên thứ ba đi
|
|
141
|
+
qua ``InterceptHandler``;
|
|
142
|
+
2. phát event ``phase_start`` / ``phase_end`` / ``phase_failed`` kèm
|
|
143
|
+
``duration_ms`` -- đây chính là dữ liệu để UI vẽ timeline và chỉ ra job
|
|
144
|
+
chết ở bước nào;
|
|
145
|
+
3. cung cấp phase cho classifier khi exception bay ra, mà không nơi nào phải
|
|
146
|
+
truyền tay.
|
|
147
|
+
|
|
148
|
+
Phase lồng nhau được: phase con khôi phục lại phase cha khi thoát.
|
|
149
|
+
|
|
150
|
+
@return: logger đã bind context + phase
|
|
151
|
+
"""
|
|
152
|
+
token = _PHASE.set(runtime_phase)
|
|
153
|
+
bound = logger.bind(**_JOB_CONTEXT.get(), phase=runtime_phase.value)
|
|
154
|
+
started = time.perf_counter()
|
|
155
|
+
bound.bind(event='phase_start').info(f'{runtime_phase.value} started')
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
yield bound
|
|
159
|
+
except BaseException:
|
|
160
|
+
# Ghi lại để khối except bên ngoài -- nơi _PHASE đã bị reset -- vẫn biết
|
|
161
|
+
# lỗi thoát ra từ phase nào.
|
|
162
|
+
_FAILED_PHASE.set(runtime_phase)
|
|
163
|
+
elapsed = _elapsed_ms(started)
|
|
164
|
+
# WARNING chứ không phải DEBUG: đây là mốc timeline, phải thấy được khi
|
|
165
|
+
# chạy ở LOG_LEVEL=INFO. Không kèm traceback vì build_error_job_response
|
|
166
|
+
# sẽ log exception đầy đủ ngay sau đó -- tránh in stack hai lần.
|
|
167
|
+
bound.bind(event='phase_failed', duration_ms=elapsed).warning(
|
|
168
|
+
f'{runtime_phase.value} failed after {elapsed} ms')
|
|
169
|
+
raise
|
|
170
|
+
else:
|
|
171
|
+
elapsed = _elapsed_ms(started)
|
|
172
|
+
bound.bind(event='phase_end', duration_ms=elapsed).info(
|
|
173
|
+
f'{runtime_phase.value} completed in {elapsed} ms')
|
|
174
|
+
finally:
|
|
175
|
+
_PHASE.reset(token)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _elapsed_ms(started: float) -> float:
|
|
179
|
+
return round((time.perf_counter() - started) * 1000, 2)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def reset() -> None:
|
|
183
|
+
"""Xoá context. Chủ yếu dùng trong test."""
|
|
184
|
+
_JOB_CONTEXT.set({})
|
|
185
|
+
_PHASE.set(None)
|
|
186
|
+
_FAILED_PHASE.set(None)
|
|
@@ -90,8 +90,35 @@ def redact(text) -> str:
|
|
|
90
90
|
EXC_TEXT = '_exc_text'
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def _inject_job_context(record) -> None:
|
|
94
|
+
"""Bổ sung job context vào record nếu chưa có.
|
|
95
|
+
|
|
96
|
+
Cần thiết vì không phải log nào cũng đi qua ``job_logger()``. Có ba đường
|
|
97
|
+
vào và cả ba phải có context:
|
|
98
|
+
|
|
99
|
+
- ``job_logger()`` -- đã bind sẵn, không đụng tới;
|
|
100
|
+
- stdlib ``logging`` qua ``InterceptHandler`` -- handler tự bind;
|
|
101
|
+
- **raw loguru** ``from quapp_common.config.logging_config import logger`` --
|
|
102
|
+
đường này 10 template và nhiều module trong lib đang dùng
|
|
103
|
+
(``logger.bind(context='Provider')``, ``dispatcher``, ``bridge``...), và
|
|
104
|
+
``extra`` của nó không có job_id.
|
|
105
|
+
|
|
106
|
+
Đọc contextvars ở đây (thời điểm emit) thay vì lúc lấy logger, nên đúng cả
|
|
107
|
+
khi logger là biến module-level được tạo một lần lúc import.
|
|
108
|
+
"""
|
|
109
|
+
from .log_context import current_phase_value, job_context
|
|
110
|
+
|
|
111
|
+
extra = record['extra']
|
|
112
|
+
for key, value in job_context().items():
|
|
113
|
+
if value is not None and extra.get(key) is None:
|
|
114
|
+
extra[key] = value
|
|
115
|
+
|
|
116
|
+
if extra.get('phase') is None:
|
|
117
|
+
extra['phase'] = current_phase_value()
|
|
118
|
+
|
|
119
|
+
|
|
93
120
|
def _prepare_record(record) -> bool:
|
|
94
|
-
"""loguru filter: mask message và pre-format traceback.
|
|
121
|
+
"""loguru filter: bổ sung context, mask message và pre-format traceback.
|
|
95
122
|
|
|
96
123
|
Filter chạy ở process gọi log, TRƯỚC khi record được đưa vào queue của
|
|
97
124
|
``enqueue=True``. Đây là chỗ duy nhất còn nhìn thấy traceback object thật --
|
|
@@ -100,6 +127,7 @@ def _prepare_record(record) -> bool:
|
|
|
100
127
|
|
|
101
128
|
Trả True để giữ record.
|
|
102
129
|
"""
|
|
130
|
+
_inject_job_context(record)
|
|
103
131
|
record['message'] = redact(record['message'])
|
|
104
132
|
|
|
105
133
|
exception = record['exception']
|
|
@@ -191,7 +219,7 @@ class InterceptHandler(logging.Handler):
|
|
|
191
219
|
"""
|
|
192
220
|
|
|
193
221
|
def emit(self, record: logging.LogRecord) -> None:
|
|
194
|
-
from .log_context import
|
|
222
|
+
from .log_context import current_phase_value, job_context
|
|
195
223
|
|
|
196
224
|
try:
|
|
197
225
|
level = logger.level(record.levelname).name
|
|
@@ -209,7 +237,7 @@ class InterceptHandler(logging.Handler):
|
|
|
209
237
|
depth += 1
|
|
210
238
|
|
|
211
239
|
logger.opt(depth=depth, exception=record.exc_info).bind(
|
|
212
|
-
**job_context(), phase=
|
|
240
|
+
**job_context(), phase=current_phase_value(),
|
|
213
241
|
stdlib_logger=record.name).log(level, record.getMessage())
|
|
214
242
|
|
|
215
243
|
|
|
@@ -264,13 +292,13 @@ def job_logger(job_id: str = None):
|
|
|
264
292
|
Khác biệt so với trước: khi không truyền ``job_id``, lấy context đã bind bởi
|
|
265
293
|
``bind_job()`` thay vì rơi về ``"QuappLibs"``.
|
|
266
294
|
"""
|
|
267
|
-
from .log_context import
|
|
295
|
+
from .log_context import current_phase_value, job_context
|
|
268
296
|
|
|
269
297
|
context = job_context()
|
|
270
298
|
if job_id:
|
|
271
299
|
context = {**context, 'job_id': job_id}
|
|
272
300
|
|
|
273
|
-
return logger.bind(**context, phase=
|
|
301
|
+
return logger.bind(**context, phase=current_phase_value())
|
|
274
302
|
|
|
275
303
|
|
|
276
304
|
configure_logging()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QApp Platform Project thread_config.py Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
3
|
+
"""
|
|
4
|
+
import contextvars
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContextAwareThreadPoolExecutor(ThreadPoolExecutor):
|
|
9
|
+
"""ThreadPoolExecutor giữ lại contextvars của caller.
|
|
10
|
+
|
|
11
|
+
``ThreadPoolExecutor.submit()`` KHÔNG copy context (khác ``asyncio.to_thread``
|
|
12
|
+
và ``starlette.run_in_threadpool``). Cả ``JobFetcher._handle_successful_job``
|
|
13
|
+
lẫn ``JobFetching.fetch`` đều submit phần analysis + finalization vào
|
|
14
|
+
``circuit_running_pool``, nên nếu không copy context thì toàn bộ log của hai
|
|
15
|
+
phase đó mất ``job_id`` / ``trace_id`` / ``phase`` và rơi về bucket
|
|
16
|
+
"QuappLibs".
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def submit(self, fn, /, *args, **kwargs):
|
|
20
|
+
return super().submit(contextvars.copy_context().run, fn, *args,
|
|
21
|
+
**kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
circuit_exporting_pool = ContextAwareThreadPoolExecutor(
|
|
25
|
+
max_workers=10, thread_name_prefix='circuit-exporting-pool-')
|
|
26
|
+
|
|
27
|
+
circuit_running_pool = ContextAwareThreadPoolExecutor(
|
|
28
|
+
max_workers=20, thread_name_prefix='circuit-running-pool-')
|