quapp-common 0.0.13.dev9__tar.gz → 0.0.14.dev2__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.dev2}/PKG-INFO +114 -2
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/README.md +113 -1
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/pyproject.toml +1 -1
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/post_processing_task.py +3 -4
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/invocation.py +24 -20
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetcher.py +5 -8
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetching.py +5 -10
- quapp_common-0.0.14.dev2/quapp_common/config/log_context.py +186 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/config/logging_config.py +63 -11
- quapp_common-0.0.14.dev2/quapp_common/config/thread_config.py +28 -0
- quapp_common-0.0.14.dev2/quapp_common/data/response/error_report.py +191 -0
- quapp_common-0.0.14.dev2/quapp_common/enum/error_category.py +91 -0
- quapp_common-0.0.14.dev2/quapp_common/enum/runtime_phase.py +118 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/device/custom_device.py +4 -5
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/device/device.py +48 -32
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/invocation.py +13 -1
- quapp_common-0.0.14.dev2/quapp_common/util/error_classifier.py +456 -0
- quapp_common-0.0.14.dev2/quapp_common/util/invocation_failure.py +103 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/util/response_utils.py +21 -6
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2/quapp_common.egg-info}/PKG-INFO +114 -2
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/SOURCES.txt +10 -0
- quapp_common-0.0.14.dev2/tests/test_error_classifier.py +382 -0
- quapp_common-0.0.14.dev2/tests/test_error_log_dedupe.py +176 -0
- quapp_common-0.0.14.dev2/tests/test_error_report.py +405 -0
- quapp_common-0.0.14.dev2/tests/test_invocation_failure.py +148 -0
- quapp_common-0.0.14.dev2/tests/test_log_secret_leak.py +160 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/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.dev2}/LICENSE +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_invocation_task.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/bridge.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/dispatcher.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/setup.cfg +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/tests/test_dispatcher_runner_error.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/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.dev2
|
|
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,119 @@ Notes:
|
|
|
114
114
|
|
|
115
115
|
## Recently Changes Highlights
|
|
116
116
|
|
|
117
|
-
### v0.0.14.
|
|
117
|
+
### v0.0.14.dev2 — Fix: log lỗi bị nhân đôi
|
|
118
|
+
|
|
119
|
+
- **Fix**: mỗi lần lỗi phát **hai** dòng ERROR cùng `event=job_failed`, mỗi dòng
|
|
120
|
+
kèm một traceback riêng. Nguyên nhân: `build_error_job_response()` (thêm ở
|
|
121
|
+
`0.0.13.dev9`) tự phát một dòng log tập trung, nhưng 10 call-site vẫn giữ
|
|
122
|
+
`self.logger.exception(...)` của chúng.
|
|
123
|
+
|
|
124
|
+
Ngoài việc làm ồn Function Log và nhân đôi lượng traceback, nó **làm sai metric
|
|
125
|
+
nếu backend đếm `event=job_failed`** để tính tỉ lệ job thất bại.
|
|
126
|
+
|
|
127
|
+
Đã gỡ log ở 10 call-site trong `device.py`, `custom_device.py`,
|
|
128
|
+
`invocation.py` (×3), `job_fetcher.py` (×2), `job_fetching.py` (×2),
|
|
129
|
+
`post_processing_task.py`. Dòng log tập trung mang nhiều thông tin hơn bản bị
|
|
130
|
+
gỡ: `event` / `category` / `error_code` / `phase` + full traceback.
|
|
131
|
+
|
|
132
|
+
`phase_failed` (WARNING, mốc timeline) là event khác, không bị tính là trùng.
|
|
133
|
+
|
|
134
|
+
- **Fix (security)**: credential lọt vào log text qua **hai đường** mà
|
|
135
|
+
`redact()` không chặn được:
|
|
136
|
+
|
|
137
|
+
1. `loguru diagnose=True` (default) in giá trị **biến cục bộ** trong traceback
|
|
138
|
+
dưới dạng `└ 'value'`. Không có tên key nên `redact()` — vốn match
|
|
139
|
+
`key: value` — không mask. Token provider nằm trong biến cục bộ lọt nguyên.
|
|
140
|
+
Đã set `diagnose=False` cho sink text (`backtrace=True` vẫn giữ: nó mở rộng
|
|
141
|
+
call chain nhưng không in giá trị).
|
|
142
|
+
2. Placeholder `{exception}` được loguru render **trực tiếp từ exception
|
|
143
|
+
object**, không đi qua filter. Secret nằm trong `str(exception)` — ví dụ
|
|
144
|
+
response body của provider bị nhét vào `ValueError` — lọt nguyên. Sink text
|
|
145
|
+
nay append traceback đã redact ở `extra[_exc_text]` thay cho placeholder đó.
|
|
146
|
+
|
|
147
|
+
Sink JSON không bị ảnh hưởng: nó đã dùng `_exc_text` và `diagnose=False`.
|
|
148
|
+
|
|
149
|
+
- **Docs**: ghi rõ trong docstring nên chọn `QUAPP_LOG_FORMAT` nào. Với
|
|
150
|
+
Grafana/Loki phải dùng `json`: ở chế độ `text`, traceback là các dòng **riêng
|
|
151
|
+
không có prefix** `[ConsoleJobLog][<job_id>]`, nên filter theo job_id sẽ mất
|
|
152
|
+
toàn bộ stack. `both` ghi mỗi record hai lần — chỉ dùng khi debug.
|
|
153
|
+
|
|
154
|
+
- **Test**: thêm `tests/test_log_secret_leak.py` — 5 case, test qua sink THẬT do
|
|
155
|
+
`configure_logging()` tạo (không introspect internals). Có một case khoá lại
|
|
156
|
+
hành vi `diagnose=True` của loguru, để nếu upstream đổi thì ta biết.
|
|
157
|
+
|
|
158
|
+
- **Test**: thêm `tests/test_error_log_dedupe.py` — khoá hồi quy ở hai mức: đếm
|
|
159
|
+
số dòng `job_failed` khi chạy thật, **và** quét source để bắt trường hợp ai đó
|
|
160
|
+
thêm lại `logger.exception()` ngay trước `build_error_job_response()`.
|
|
161
|
+
|
|
162
|
+
Kèm test cho trường hợp **nhiều job đồng thời**: 4 job qua
|
|
163
|
+
`run_in_threadpool` + `circuit_running_pool` phải có số dòng log đều nhau,
|
|
164
|
+
không job nào nhận log của job khác, và `phase` giữ được khi đi vào thread
|
|
165
|
+
pool.
|
|
166
|
+
|
|
167
|
+
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
168
|
+
|
|
169
|
+
Tiếp nối Phase 0. Bổ sung phân loại lỗi có ngữ nghĩa và envelope dùng chung cho
|
|
170
|
+
Function Log lẫn Job Log. **Additive**: `job_result` chỉ thêm field `error`,
|
|
171
|
+
`message` và `exception` giữ nguyên; contract callback URL không đổi.
|
|
172
|
+
|
|
173
|
+
**Taxonomy — hai chiều phân loại độc lập**
|
|
174
|
+
- `enum/error_category.py`: `ErrorCategory` 13 giá trị, nhóm theo *ai xử lý
|
|
175
|
+
được* (`owner_of()` → USER / WORKSPACE_ADMIN / PROVIDER / PLATFORM).
|
|
176
|
+
- `enum/runtime_phase.py`: `RuntimePhase` 9 giá trị. Tách được
|
|
177
|
+
`PROVIDER_INVOCATION` / `COMPILATION` / `EXECUTION` — trước đây auth sai,
|
|
178
|
+
transpile fail và provider từ chối job đều ra chung nhãn `EXECUTION`.
|
|
179
|
+
`PHASE_TO_STEP` giữ nguyên contract `InvocationStep` của callback URL.
|
|
180
|
+
|
|
181
|
+
**Classifier (`util/error_classifier.py`)** — tổ hợp 5 tín hiệu theo độ tin cậy:
|
|
182
|
+
1. `phase` — biết chắc từ context manager, không suy đoán;
|
|
183
|
+
2. `frame_owner()` — code của ai, dựa vào đường dẫn frame sâu nhất
|
|
184
|
+
(`function/` = user, `quapp_*` = platform, `site-packages/` = SDK);
|
|
185
|
+
3. tên class exception, đi hết **cause chain** qua cả `__cause__` *và*
|
|
186
|
+
`__context__` — cần thiết vì nhiều provider lib bọc lỗi bằng
|
|
187
|
+
`raise ValueError(...)` trong khối `except` mà không có `from`;
|
|
188
|
+
4. attribute đọc được: HTTP status, AWS error code, exit code, signal;
|
|
189
|
+
5. message pattern (dùng cuối cùng).
|
|
190
|
+
|
|
191
|
+
`register_rules()` cho phép lib provider đăng ký rule đặc thù SDK khi import,
|
|
192
|
+
nên `qapp-common` không phải import qiskit/braket. Batch đăng ký sau được xét
|
|
193
|
+
trước; trong cùng batch giữ nguyên thứ tự. Rule ném lỗi bị bỏ qua chứ không làm
|
|
194
|
+
hỏng đường báo lỗi. 20 rule generic sẵn có.
|
|
195
|
+
|
|
196
|
+
Fallback nhiều tầng nên **không bao giờ trả `UNKNOWN` một cách mù**: không rule
|
|
197
|
+
nào khớp thì suy từ frame ownership, rồi tới phase.
|
|
198
|
+
|
|
199
|
+
**Envelope (`data/response/error_report.py`)** — `job_result['error']` gồm
|
|
200
|
+
`code`, `category`, `phase`, `owner`, `retryable`, `rootCause` (type + detail +
|
|
201
|
+
file:line), `causeChain`, `stackTrace` (full, đã redact),
|
|
202
|
+
`handlerStackTrace` (stack JS/.NET cho handler không phải Python),
|
|
203
|
+
`context`, `resolution` (summary + steps + docUrl), `retry`, `traceId`.
|
|
204
|
+
`log_fields()` bind cùng vocabulary vào Function Log.
|
|
205
|
+
|
|
206
|
+
**Timeline (`config/log_context.py`)** — context manager `phase()` phát
|
|
207
|
+
`phase_start` / `phase_end` / `phase_failed` kèm `duration_ms`. Đây là dữ liệu
|
|
208
|
+
để UI vẽ timeline và chỉ ra job chết ở bước nào. `phase_failed` ở mức WARNING để
|
|
209
|
+
thấy được khi chạy `LOG_LEVEL=INFO`.
|
|
210
|
+
|
|
211
|
+
`_FAILED_PHASE` ghi lại phase mà exception thoát ra: khối `except` xử lý lỗi gần
|
|
212
|
+
như luôn nằm **ngoài** `with phase(...)`, lúc đó phase đang mở đã bị reset trong
|
|
213
|
+
`finally`. Không có nó thì mọi `ErrorReport` đều `phase=None` và fallback theo
|
|
214
|
+
phase không bao giờ chạy. Dùng `effective_phase()` ở đường báo lỗi.
|
|
215
|
+
|
|
216
|
+
**`config/thread_config.py`** — `ContextAwareThreadPoolExecutor` copy contextvars
|
|
217
|
+
khi submit. `ThreadPoolExecutor` thuần không làm việc này (khác
|
|
218
|
+
`asyncio.to_thread`), nên analysis + finalization chạy trong
|
|
219
|
+
`circuit_running_pool` sẽ mất `job_id` / `trace_id` / `phase`.
|
|
220
|
+
|
|
221
|
+
**Call-site** — `phase()` bọc PREPROCESSING / DEVICE_SELECTION /
|
|
222
|
+
PROVIDER_INVOCATION / COMPILATION / EXECUTION / ANALYSIS; toàn bộ 13 call-site
|
|
223
|
+
của `build_error_job_response()` truyền `phase=`.
|
|
224
|
+
|
|
225
|
+
**Test** — `tests/test_error_classifier.py` (52 case: cause chain, HTTP/AWS
|
|
226
|
+
code, frame ownership, phase fallback, rule ordering) và
|
|
227
|
+
`tests/test_error_report.py` (44 case: envelope, timeline, thread pool).
|
|
228
|
+
|
|
229
|
+
### v0.0.13.dev9 — Error logging & observability (Phase 0)
|
|
118
230
|
|
|
119
231
|
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
232
|
đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
|
|
@@ -73,7 +73,119 @@ Notes:
|
|
|
73
73
|
|
|
74
74
|
## Recently Changes Highlights
|
|
75
75
|
|
|
76
|
-
### v0.0.14.
|
|
76
|
+
### v0.0.14.dev2 — Fix: log lỗi bị nhân đôi
|
|
77
|
+
|
|
78
|
+
- **Fix**: mỗi lần lỗi phát **hai** dòng ERROR cùng `event=job_failed`, mỗi dòng
|
|
79
|
+
kèm một traceback riêng. Nguyên nhân: `build_error_job_response()` (thêm ở
|
|
80
|
+
`0.0.13.dev9`) tự phát một dòng log tập trung, nhưng 10 call-site vẫn giữ
|
|
81
|
+
`self.logger.exception(...)` của chúng.
|
|
82
|
+
|
|
83
|
+
Ngoài việc làm ồn Function Log và nhân đôi lượng traceback, nó **làm sai metric
|
|
84
|
+
nếu backend đếm `event=job_failed`** để tính tỉ lệ job thất bại.
|
|
85
|
+
|
|
86
|
+
Đã gỡ log ở 10 call-site trong `device.py`, `custom_device.py`,
|
|
87
|
+
`invocation.py` (×3), `job_fetcher.py` (×2), `job_fetching.py` (×2),
|
|
88
|
+
`post_processing_task.py`. Dòng log tập trung mang nhiều thông tin hơn bản bị
|
|
89
|
+
gỡ: `event` / `category` / `error_code` / `phase` + full traceback.
|
|
90
|
+
|
|
91
|
+
`phase_failed` (WARNING, mốc timeline) là event khác, không bị tính là trùng.
|
|
92
|
+
|
|
93
|
+
- **Fix (security)**: credential lọt vào log text qua **hai đường** mà
|
|
94
|
+
`redact()` không chặn được:
|
|
95
|
+
|
|
96
|
+
1. `loguru diagnose=True` (default) in giá trị **biến cục bộ** trong traceback
|
|
97
|
+
dưới dạng `└ 'value'`. Không có tên key nên `redact()` — vốn match
|
|
98
|
+
`key: value` — không mask. Token provider nằm trong biến cục bộ lọt nguyên.
|
|
99
|
+
Đã set `diagnose=False` cho sink text (`backtrace=True` vẫn giữ: nó mở rộng
|
|
100
|
+
call chain nhưng không in giá trị).
|
|
101
|
+
2. Placeholder `{exception}` được loguru render **trực tiếp từ exception
|
|
102
|
+
object**, không đi qua filter. Secret nằm trong `str(exception)` — ví dụ
|
|
103
|
+
response body của provider bị nhét vào `ValueError` — lọt nguyên. Sink text
|
|
104
|
+
nay append traceback đã redact ở `extra[_exc_text]` thay cho placeholder đó.
|
|
105
|
+
|
|
106
|
+
Sink JSON không bị ảnh hưởng: nó đã dùng `_exc_text` và `diagnose=False`.
|
|
107
|
+
|
|
108
|
+
- **Docs**: ghi rõ trong docstring nên chọn `QUAPP_LOG_FORMAT` nào. Với
|
|
109
|
+
Grafana/Loki phải dùng `json`: ở chế độ `text`, traceback là các dòng **riêng
|
|
110
|
+
không có prefix** `[ConsoleJobLog][<job_id>]`, nên filter theo job_id sẽ mất
|
|
111
|
+
toàn bộ stack. `both` ghi mỗi record hai lần — chỉ dùng khi debug.
|
|
112
|
+
|
|
113
|
+
- **Test**: thêm `tests/test_log_secret_leak.py` — 5 case, test qua sink THẬT do
|
|
114
|
+
`configure_logging()` tạo (không introspect internals). Có một case khoá lại
|
|
115
|
+
hành vi `diagnose=True` của loguru, để nếu upstream đổi thì ta biết.
|
|
116
|
+
|
|
117
|
+
- **Test**: thêm `tests/test_error_log_dedupe.py` — khoá hồi quy ở hai mức: đếm
|
|
118
|
+
số dòng `job_failed` khi chạy thật, **và** quét source để bắt trường hợp ai đó
|
|
119
|
+
thêm lại `logger.exception()` ngay trước `build_error_job_response()`.
|
|
120
|
+
|
|
121
|
+
Kèm test cho trường hợp **nhiều job đồng thời**: 4 job qua
|
|
122
|
+
`run_in_threadpool` + `circuit_running_pool` phải có số dòng log đều nhau,
|
|
123
|
+
không job nào nhận log của job khác, và `phase` giữ được khi đi vào thread
|
|
124
|
+
pool.
|
|
125
|
+
|
|
126
|
+
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
127
|
+
|
|
128
|
+
Tiếp nối Phase 0. Bổ sung phân loại lỗi có ngữ nghĩa và envelope dùng chung cho
|
|
129
|
+
Function Log lẫn Job Log. **Additive**: `job_result` chỉ thêm field `error`,
|
|
130
|
+
`message` và `exception` giữ nguyên; contract callback URL không đổi.
|
|
131
|
+
|
|
132
|
+
**Taxonomy — hai chiều phân loại độc lập**
|
|
133
|
+
- `enum/error_category.py`: `ErrorCategory` 13 giá trị, nhóm theo *ai xử lý
|
|
134
|
+
được* (`owner_of()` → USER / WORKSPACE_ADMIN / PROVIDER / PLATFORM).
|
|
135
|
+
- `enum/runtime_phase.py`: `RuntimePhase` 9 giá trị. Tách được
|
|
136
|
+
`PROVIDER_INVOCATION` / `COMPILATION` / `EXECUTION` — trước đây auth sai,
|
|
137
|
+
transpile fail và provider từ chối job đều ra chung nhãn `EXECUTION`.
|
|
138
|
+
`PHASE_TO_STEP` giữ nguyên contract `InvocationStep` của callback URL.
|
|
139
|
+
|
|
140
|
+
**Classifier (`util/error_classifier.py`)** — tổ hợp 5 tín hiệu theo độ tin cậy:
|
|
141
|
+
1. `phase` — biết chắc từ context manager, không suy đoán;
|
|
142
|
+
2. `frame_owner()` — code của ai, dựa vào đường dẫn frame sâu nhất
|
|
143
|
+
(`function/` = user, `quapp_*` = platform, `site-packages/` = SDK);
|
|
144
|
+
3. tên class exception, đi hết **cause chain** qua cả `__cause__` *và*
|
|
145
|
+
`__context__` — cần thiết vì nhiều provider lib bọc lỗi bằng
|
|
146
|
+
`raise ValueError(...)` trong khối `except` mà không có `from`;
|
|
147
|
+
4. attribute đọc được: HTTP status, AWS error code, exit code, signal;
|
|
148
|
+
5. message pattern (dùng cuối cùng).
|
|
149
|
+
|
|
150
|
+
`register_rules()` cho phép lib provider đăng ký rule đặc thù SDK khi import,
|
|
151
|
+
nên `qapp-common` không phải import qiskit/braket. Batch đăng ký sau được xét
|
|
152
|
+
trước; trong cùng batch giữ nguyên thứ tự. Rule ném lỗi bị bỏ qua chứ không làm
|
|
153
|
+
hỏng đường báo lỗi. 20 rule generic sẵn có.
|
|
154
|
+
|
|
155
|
+
Fallback nhiều tầng nên **không bao giờ trả `UNKNOWN` một cách mù**: không rule
|
|
156
|
+
nào khớp thì suy từ frame ownership, rồi tới phase.
|
|
157
|
+
|
|
158
|
+
**Envelope (`data/response/error_report.py`)** — `job_result['error']` gồm
|
|
159
|
+
`code`, `category`, `phase`, `owner`, `retryable`, `rootCause` (type + detail +
|
|
160
|
+
file:line), `causeChain`, `stackTrace` (full, đã redact),
|
|
161
|
+
`handlerStackTrace` (stack JS/.NET cho handler không phải Python),
|
|
162
|
+
`context`, `resolution` (summary + steps + docUrl), `retry`, `traceId`.
|
|
163
|
+
`log_fields()` bind cùng vocabulary vào Function Log.
|
|
164
|
+
|
|
165
|
+
**Timeline (`config/log_context.py`)** — context manager `phase()` phát
|
|
166
|
+
`phase_start` / `phase_end` / `phase_failed` kèm `duration_ms`. Đây là dữ liệu
|
|
167
|
+
để UI vẽ timeline và chỉ ra job chết ở bước nào. `phase_failed` ở mức WARNING để
|
|
168
|
+
thấy được khi chạy `LOG_LEVEL=INFO`.
|
|
169
|
+
|
|
170
|
+
`_FAILED_PHASE` ghi lại phase mà exception thoát ra: khối `except` xử lý lỗi gần
|
|
171
|
+
như luôn nằm **ngoài** `with phase(...)`, lúc đó phase đang mở đã bị reset trong
|
|
172
|
+
`finally`. Không có nó thì mọi `ErrorReport` đều `phase=None` và fallback theo
|
|
173
|
+
phase không bao giờ chạy. Dùng `effective_phase()` ở đường báo lỗi.
|
|
174
|
+
|
|
175
|
+
**`config/thread_config.py`** — `ContextAwareThreadPoolExecutor` copy contextvars
|
|
176
|
+
khi submit. `ThreadPoolExecutor` thuần không làm việc này (khác
|
|
177
|
+
`asyncio.to_thread`), nên analysis + finalization chạy trong
|
|
178
|
+
`circuit_running_pool` sẽ mất `job_id` / `trace_id` / `phase`.
|
|
179
|
+
|
|
180
|
+
**Call-site** — `phase()` bọc PREPROCESSING / DEVICE_SELECTION /
|
|
181
|
+
PROVIDER_INVOCATION / COMPILATION / EXECUTION / ANALYSIS; toàn bộ 13 call-site
|
|
182
|
+
của `build_error_job_response()` truyền `phase=`.
|
|
183
|
+
|
|
184
|
+
**Test** — `tests/test_error_classifier.py` (52 case: cause chain, HTTP/AWS
|
|
185
|
+
code, frame ownership, phase fallback, rule ordering) và
|
|
186
|
+
`tests/test_error_report.py` (44 case: envelope, timeline, thread pool).
|
|
187
|
+
|
|
188
|
+
### v0.0.13.dev9 — Error logging & observability (Phase 0)
|
|
77
189
|
|
|
78
190
|
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
191
|
đổ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.dev2"
|
|
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
|
|
|
@@ -44,13 +45,11 @@ class PostProcessingTask(AsyncTask):
|
|
|
44
45
|
update_job_metadata(job_response, self.promise.callback_url.on_done)
|
|
45
46
|
|
|
46
47
|
except Exception as exception:
|
|
47
|
-
self.logger.bind(event='job_failed').exception(
|
|
48
|
-
f"Post-processing failed: {exception}")
|
|
49
|
-
|
|
50
48
|
job_response = build_error_job_response(
|
|
51
49
|
exception, job_response,
|
|
52
50
|
message='Error when post processing job result',
|
|
53
|
-
stage='while post-processing the job result'
|
|
51
|
+
stage='while post-processing the job result',
|
|
52
|
+
phase=RuntimePhase.POST_PROCESSING)
|
|
54
53
|
|
|
55
54
|
update_job_metadata(job_response,
|
|
56
55
|
self.promise.callback_url.on_error)
|
{quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/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,19 +84,20 @@ 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
|
|
|
90
96
|
except Exception as exception:
|
|
91
|
-
self.logger.exception(
|
|
92
|
-
f"Error when prepare backend data: {exception}",
|
|
93
|
-
exc_info=True)
|
|
94
|
-
|
|
95
97
|
job_response = build_error_job_response(exception,
|
|
96
98
|
message='Error when prepare backend data',
|
|
97
|
-
stage='while preparing the backend'
|
|
99
|
+
stage='while preparing the backend',
|
|
100
|
+
phase=RuntimePhase.DEVICE_SELECTION)
|
|
98
101
|
job_response.authentication = self.authentication
|
|
99
102
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
100
103
|
InvocationStep.PREPARATION).on_error)
|
|
@@ -125,19 +128,21 @@ class Invocation(ABC):
|
|
|
125
128
|
self.logger.debug(
|
|
126
129
|
f"Executing job with provider tag: {provider_tag.value}")
|
|
127
130
|
|
|
128
|
-
|
|
131
|
+
# PROVIDER_INVOCATION tách riêng khỏi EXECUTION: lỗi authenticate
|
|
132
|
+
# với provider và lỗi provider từ chối circuit là hai chuyện khác
|
|
133
|
+
# nhau, trước đây cùng ra nhãn EXECUTION.
|
|
134
|
+
with phase(RuntimePhase.PROVIDER_INVOCATION):
|
|
135
|
+
provider = self._create_provider()
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
self.logger.debug(
|
|
138
|
+
f"Executing job with device name: {device_name}")
|
|
139
|
+
device = self._create_device(provider)
|
|
132
140
|
|
|
133
141
|
except Exception as exception:
|
|
134
|
-
self.logger.exception(
|
|
135
|
-
f"Exception when create provider or device: {exception}",
|
|
136
|
-
exc_info=True)
|
|
137
|
-
|
|
138
142
|
job_response = build_error_job_response(exception,
|
|
139
143
|
message='Error when create provider or device',
|
|
140
|
-
stage='while running the circuit on the device'
|
|
144
|
+
stage='while running the circuit on the device',
|
|
145
|
+
phase=RuntimePhase.PROVIDER_INVOCATION)
|
|
141
146
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
142
147
|
InvocationStep.EXECUTION).on_error, )
|
|
143
148
|
|
|
@@ -170,7 +175,8 @@ class Invocation(ABC):
|
|
|
170
175
|
InvocationStep.PREPARATION).on_start)
|
|
171
176
|
|
|
172
177
|
try:
|
|
173
|
-
|
|
178
|
+
with phase(RuntimePhase.PREPROCESSING):
|
|
179
|
+
circuit = circuit_preparation_fn(self.input)
|
|
174
180
|
|
|
175
181
|
if circuit is None:
|
|
176
182
|
self.logger.warning(
|
|
@@ -185,12 +191,10 @@ class Invocation(ABC):
|
|
|
185
191
|
return circuit
|
|
186
192
|
|
|
187
193
|
except Exception as exception:
|
|
188
|
-
self.logger.exception(f'Error when prepare circuit: {exception}',
|
|
189
|
-
exc_info=True)
|
|
190
|
-
|
|
191
194
|
job_response = build_error_job_response(exception, job_response,
|
|
192
195
|
message='Error when prepare circuit',
|
|
193
|
-
stage='while preparing the circuit'
|
|
196
|
+
stage='while preparing the circuit',
|
|
197
|
+
phase=RuntimePhase.PREPROCESSING)
|
|
194
198
|
|
|
195
199
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
196
200
|
InvocationStep.PREPARATION).on_error)
|
{quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/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
|
|
@@ -127,15 +128,12 @@ class JobFetcher(ABC):
|
|
|
127
128
|
job_response.status_code = StatusCode.POLLING
|
|
128
129
|
|
|
129
130
|
except Exception as exception:
|
|
130
|
-
self.logger.exception(
|
|
131
|
-
f"Exception during job fetch for provider_job_id {self.provider_job_id}: {exception}",
|
|
132
|
-
exc_info=True)
|
|
133
|
-
|
|
134
131
|
job_response = build_error_job_response(
|
|
135
132
|
exception, job_response,
|
|
136
133
|
message='Error when fetching job with provider_job_id {0}'.format(
|
|
137
134
|
self.provider_job_id),
|
|
138
|
-
stage='while fetching the job result from the provider'
|
|
135
|
+
stage='while fetching the job result from the provider',
|
|
136
|
+
phase=RuntimePhase.POLLING)
|
|
139
137
|
update_job_metadata(job_response, self.callback_urls[
|
|
140
138
|
InvocationStep.EXECUTION].on_error)
|
|
141
139
|
|
|
@@ -207,13 +205,12 @@ class JobFetcher(ABC):
|
|
|
207
205
|
return job_response
|
|
208
206
|
|
|
209
207
|
except Exception as exception:
|
|
210
|
-
self.logger.exception(f"Exception during analysis: {exception}",
|
|
211
|
-
exc_info=True)
|
|
212
208
|
from ...util.response_utils import build_error_job_response
|
|
213
209
|
job_response = build_error_job_response(
|
|
214
210
|
exception, job_response,
|
|
215
211
|
message='Error when analyzing job result',
|
|
216
|
-
stage='while analysing the job result'
|
|
212
|
+
stage='while analysing the job result',
|
|
213
|
+
phase=RuntimePhase.ANALYSIS)
|
|
217
214
|
|
|
218
215
|
update_job_metadata(job_response, callback_url.on_error)
|
|
219
216
|
return None
|
{quapp_common-0.0.13.dev9 → quapp_common-0.0.14.dev2}/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
|
|
@@ -89,15 +90,12 @@ class JobFetching(ABC):
|
|
|
89
90
|
job_response.status_code = StatusCode.POLLING
|
|
90
91
|
|
|
91
92
|
except Exception as exception:
|
|
92
|
-
self.logger.bind(event='job_failed').exception(
|
|
93
|
-
"Exception when fetch job with provider_job_id {0}: {1}".format(
|
|
94
|
-
self.provider_job_id, str(exception)))
|
|
95
|
-
|
|
96
93
|
job_response = build_error_job_response(
|
|
97
94
|
exception, job_response,
|
|
98
95
|
message='Exception when fetch job with provider_job_id {0}'.format(
|
|
99
96
|
self.provider_job_id),
|
|
100
|
-
stage='while fetching the job result from the provider'
|
|
97
|
+
stage='while fetching the job result from the provider',
|
|
98
|
+
phase=RuntimePhase.POLLING)
|
|
101
99
|
|
|
102
100
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
103
101
|
InvocationStep.EXECUTION).on_error)
|
|
@@ -154,15 +152,12 @@ class JobFetching(ABC):
|
|
|
154
152
|
return job_response
|
|
155
153
|
|
|
156
154
|
except Exception as exception:
|
|
157
|
-
self.logger.exception(
|
|
158
|
-
"Exception when analyst job result with provider_job_id {0}: {1}".format(
|
|
159
|
-
self.provider_job_id, str(exception)))
|
|
160
|
-
|
|
161
155
|
job_response = build_error_job_response(
|
|
162
156
|
exception, job_response,
|
|
163
157
|
message='Exception when analyst job result with provider_job_id {0}'.format(
|
|
164
158
|
self.provider_job_id),
|
|
165
|
-
stage='while analysing the job result'
|
|
159
|
+
stage='while analysing the job result',
|
|
160
|
+
phase=RuntimePhase.ANALYSIS)
|
|
166
161
|
|
|
167
162
|
update_job_metadata(job_response, callback_url.on_error)
|
|
168
163
|
|
|
@@ -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)
|