quapp-common 0.0.13.dev8__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.
Files changed (106) hide show
  1. {quapp_common-0.0.13.dev8/quapp_common.egg-info → quapp_common-0.0.14.dev1}/PKG-INFO +135 -1
  2. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/README.md +134 -0
  3. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/pyproject.toml +1 -1
  4. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/async_invocation_task.py +11 -7
  5. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/post_processing_task.py +13 -4
  6. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/invocation.py +24 -9
  7. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_fetcher.py +12 -5
  8. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_fetching.py +20 -12
  9. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/bridge.py +18 -20
  10. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/device/device_selection.py +15 -5
  11. quapp_common-0.0.14.dev1/quapp_common/component/dispatcher.py +272 -0
  12. quapp_common-0.0.14.dev1/quapp_common/config/log_context.py +186 -0
  13. quapp_common-0.0.14.dev1/quapp_common/config/logging_config.py +304 -0
  14. quapp_common-0.0.14.dev1/quapp_common/config/thread_config.py +28 -0
  15. quapp_common-0.0.14.dev1/quapp_common/data/response/error_report.py +191 -0
  16. quapp_common-0.0.14.dev1/quapp_common/enum/error_category.py +91 -0
  17. quapp_common-0.0.14.dev1/quapp_common/enum/runtime_phase.py +118 -0
  18. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/handler/handler.py +6 -1
  19. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/device/custom_device.py +7 -2
  20. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/device/device.py +62 -35
  21. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/invocation.py +13 -1
  22. quapp_common-0.0.14.dev1/quapp_common/util/error_classifier.py +456 -0
  23. quapp_common-0.0.14.dev1/quapp_common/util/invocation_failure.py +103 -0
  24. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/util/response_utils.py +75 -19
  25. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1/quapp_common.egg-info}/PKG-INFO +135 -1
  26. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/SOURCES.txt +11 -0
  27. quapp_common-0.0.14.dev1/tests/test_dispatcher_runner_error.py +196 -0
  28. quapp_common-0.0.14.dev1/tests/test_error_classifier.py +382 -0
  29. quapp_common-0.0.14.dev1/tests/test_error_report.py +405 -0
  30. quapp_common-0.0.14.dev1/tests/test_invocation_failure.py +148 -0
  31. quapp_common-0.0.14.dev1/tests/test_logging_config.py +297 -0
  32. quapp_common-0.0.13.dev8/quapp_common/component/dispatcher.py +0 -123
  33. quapp_common-0.0.13.dev8/quapp_common/config/logging_config.py +0 -33
  34. quapp_common-0.0.13.dev8/quapp_common/config/thread_config.py +0 -10
  35. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/LICENSE +0 -0
  36. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/__init__.py +0 -0
  37. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/__init__.py +0 -0
  38. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/async_task.py +0 -0
  39. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  40. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/__init__.py +0 -0
  41. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/__init__.py +0 -0
  42. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_heartbeat.py +0 -0
  43. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/backend/job_manager.py +0 -0
  44. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/callback/__init__.py +0 -0
  45. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/callback/update_job_metadata.py +0 -0
  46. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/circuit_adapter.py +0 -0
  47. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/device/__init__.py +0 -0
  48. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/component/result_serializer.py +0 -0
  49. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/config/__init__.py +0 -0
  50. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/__init__.py +0 -0
  51. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/__init__.py +0 -0
  52. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  53. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  54. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  55. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/backend/__init__.py +0 -0
  56. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/backend/backend_information.py +0 -0
  57. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/callback/__init__.py +0 -0
  58. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/callback/callback_url.py +0 -0
  59. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/device/__init__.py +0 -0
  60. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/device/circuit_running_option.py +0 -0
  61. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/promise/__init__.py +0 -0
  62. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/promise/post_processing_promise.py +0 -0
  63. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/promise/promise.py +0 -0
  64. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/request/__init__.py +0 -0
  65. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/request/invocation_request.py +0 -0
  66. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/request/job_fetching_request.py +0 -0
  67. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/request/request.py +0 -0
  68. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/response/__init__.py +0 -0
  69. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/response/authentication.py +0 -0
  70. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/response/custom_header.py +0 -0
  71. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/data/response/job_response.py +0 -0
  72. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/__init__.py +0 -0
  73. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/base_enum.py +0 -0
  74. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/http_header.py +0 -0
  75. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/invocation_step.py +0 -0
  76. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/language.py +0 -0
  77. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/media_type.py +0 -0
  78. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/processing_unit.py +0 -0
  79. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/provider_tag.py +0 -0
  80. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/sdk.py +0 -0
  81. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/status/__init__.py +0 -0
  82. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/status/job_status.py +0 -0
  83. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/status/status_code.py +0 -0
  84. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/enum/token_type.py +0 -0
  85. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/factory/__init__.py +0 -0
  86. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/factory/device_factory.py +0 -0
  87. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/factory/handler_factory.py +0 -0
  88. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/factory/provider_factory.py +0 -0
  89. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/handler/__init__.py +0 -0
  90. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/__init__.py +0 -0
  91. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/device/__init__.py +0 -0
  92. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/provider/__init__.py +0 -0
  93. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/model/provider/provider.py +0 -0
  94. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/util/__init__.py +0 -0
  95. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/util/file_utils.py +0 -0
  96. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/util/http_utils.py +0 -0
  97. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common/util/json_parser_utils.py +0 -0
  98. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/dependency_links.txt +0 -0
  99. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/requires.txt +0 -0
  100. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/quapp_common.egg-info/top_level.txt +0 -0
  101. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/setup.cfg +0 -0
  102. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/tests/test_circuit_adapter.py +0 -0
  103. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/tests/test_job_heartbeat.py +0 -0
  104. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/tests/test_json_parser_utils.py +0 -0
  105. {quapp_common-0.0.13.dev8 → quapp_common-0.0.14.dev1}/tests/test_language_dispatcher.py +0 -0
  106. {quapp_common-0.0.13.dev8 → 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.13.dev8
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,6 +114,140 @@ Notes:
114
114
 
115
115
  ## Recently Changes Highlights
116
116
 
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)
180
+
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
182
+ đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
183
+ không đổi schema `job_result`, không cần frontend/backend sửa gì.
184
+
185
+ **Logging (`config/`)**
186
+ - Thêm `config/log_context.py`: `bind_job()` / `enrich_job_context()` đặt
187
+ `job_id`, `trace_id`, `sdk`, `provider`, `device` vào `contextvars`. Mọi log
188
+ sau đó tự có đủ field, kể cả log của `Provider` / `DeviceSelection` /
189
+ `*Factory` — những chỗ trước đây rơi vào context `"QuappLibs"` nên không gắn
190
+ được vào job nào.
191
+ - `configure_logging()` **idempotent**. Trước đây `AsyncInvocationTask.do()` gọi
192
+ `logger.add(sink=sys.stderr, ...)` cho mỗi request, nên sau N job có N+1 sink
193
+ và mỗi dòng log in N+1 lần.
194
+ - Thêm `InterceptHandler` + `install_stdlib_intercept()`: đẩy mọi record của
195
+ stdlib `logging` sang loguru. Cần thiết vì 24/34 template trong
196
+ `quapp-sdk-templates` dùng `logging.getLogger('index')` mà không cấu hình
197
+ handler — `debug`/`info` của chúng bị drop im lặng và `exception` in ra không
198
+ có prefix `[ConsoleJobLog]` lẫn `job_id`. Log của uvicorn/qiskit/botocore cũng
199
+ về cùng một format.
200
+ - Thêm JSON sink cho structured logging, bật bằng `QUAPP_LOG_FORMAT=json|both`
201
+ (default `text`, giữ nguyên hành vi cũ). Field: `jobId`, `traceId`, `phase`,
202
+ `category`, `errorCode`, `sdk`, `provider`, `device`, `event`, `durationMs`.
203
+ - Thêm redaction filter: mask `token` / `apiKey` / `authentication` /
204
+ `clientSecret` / … trước khi ghi log, áp dụng cả cho stack trace.
205
+ - Fix: text formatter thiếu `{exception}`. Với formatter dạng callable, loguru
206
+ **không** tự append traceback — nên trước đây mọi `logger.exception()` trong
207
+ codebase không in ra stack trace nào.
208
+ - Fix: `depth` trong intercept khiến mọi record hiện là
209
+ `logging:callHandlers:<line>` thay vì file:line của nơi gọi log.
210
+ - Fix: với `enqueue=True`, traceback object không picklable nên bị thay bằng
211
+ `None` sau khi qua queue. Traceback nay được format sẵn ở filter.
212
+
213
+ **Error từ runner không phải Python (`component/dispatcher.py`, `bridge.py`)**
214
+ - Bỏ `subprocess.run(..., check=True)`. Nó raise `CalledProcessError` **trước
215
+ khi** đọc được output, làm nhánh `_extract_error_message()` thành dead code và
216
+ user chỉ thấy `Command '[...]' returned non-zero exit status 1`.
217
+ - Thêm `HandlerRuntimeError` mang `language`, `action`, `error_type`, `stack`
218
+ (stack của runtime gốc — JS/.NET), `exit_code`, `signal_name`. Sáu tình huống
219
+ được phân biệt: runner không có trong image, timeout, bị signal (handler.c
220
+ segfault → returncode âm, stdout rỗng), structured error trên stdout, exit
221
+ code khác 0, và output không phải JSON hợp lệ.
222
+ - Đọc được **cả contract cũ** (`error` dạng string, stack trên stderr) nên không
223
+ cần deploy đồng thời với template.
224
+ - `_format_exception()` đưa `handlerStackTrace` / `language` / `action` /
225
+ `exitCode` / `signal` lên `job_result` cho lỗi handler JS/Q#/C.
226
+
227
+ **Đường lỗi (`model/device/device.py`, `util/response_utils.py`, …)**
228
+ - `Device._on_execution` dùng `build_error_job_response()` thay cho
229
+ `job_result = {"error": str(exception)}` và log ở `ERROR` thay vì `DEBUG`.
230
+ Đây là đường lỗi quan trọng nhất (submit circuit lên device) và trước đây là
231
+ đường mất thông tin nhiều nhất.
232
+ - `build_error_job_response()` nay tự phát một log `ERROR` duy nhất cho mọi lỗi,
233
+ nên Job Log và Function Log không còn lệch nhau.
234
+ - Truyền `stage` cho 8 call-site còn thiếu (trước đây 10/13 call-site rơi vào
235
+ default `'while running your job'`).
236
+ - Fix `generate_response()`: nhánh `else` truy cập `job_response.status_code`
237
+ khi `job_response` là falsy → `AttributeError`, thay lỗi gốc bằng lỗi khác.
238
+ - Fix `job_fetching.py`: `__produce_histogram_data` / `__get_execution_time`
239
+ khai báo `@staticmethod` nhưng dùng `self.logger` → `NameError` khi được gọi.
240
+ - `DeviceSelection`: không log nguyên response (chứa token provider) và không
241
+ nhét `response.content` thô vào exception (nội dung này chảy vào `job_result`
242
+ rồi hiện lên UI).
243
+ - `PostProcessingTask`: bỏ `print()` ghi thẳng ra stdout (không qua sink nên
244
+ không được redact và không có `job_id`).
245
+
246
+ **Test**
247
+ - `tests/test_logging_config.py` — redaction, idempotent sink, job context,
248
+ intercept stdlib, exception payload.
249
+ - `tests/test_dispatcher_runner_error.py` — 6 nhánh phân loại lỗi runner.
250
+
117
251
  ### v0.0.13.dev2
118
252
  - Fix: `_openqasm_to_braket` in `circuit_adapter.py` now injects a default
119
253
  `Probability` result type when the Braket circuit produced by `tk_to_braket`
@@ -73,6 +73,140 @@ Notes:
73
73
 
74
74
  ## Recently Changes Highlights
75
75
 
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)
139
+
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
141
+ đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
142
+ không đổi schema `job_result`, không cần frontend/backend sửa gì.
143
+
144
+ **Logging (`config/`)**
145
+ - Thêm `config/log_context.py`: `bind_job()` / `enrich_job_context()` đặt
146
+ `job_id`, `trace_id`, `sdk`, `provider`, `device` vào `contextvars`. Mọi log
147
+ sau đó tự có đủ field, kể cả log của `Provider` / `DeviceSelection` /
148
+ `*Factory` — những chỗ trước đây rơi vào context `"QuappLibs"` nên không gắn
149
+ được vào job nào.
150
+ - `configure_logging()` **idempotent**. Trước đây `AsyncInvocationTask.do()` gọi
151
+ `logger.add(sink=sys.stderr, ...)` cho mỗi request, nên sau N job có N+1 sink
152
+ và mỗi dòng log in N+1 lần.
153
+ - Thêm `InterceptHandler` + `install_stdlib_intercept()`: đẩy mọi record của
154
+ stdlib `logging` sang loguru. Cần thiết vì 24/34 template trong
155
+ `quapp-sdk-templates` dùng `logging.getLogger('index')` mà không cấu hình
156
+ handler — `debug`/`info` của chúng bị drop im lặng và `exception` in ra không
157
+ có prefix `[ConsoleJobLog]` lẫn `job_id`. Log của uvicorn/qiskit/botocore cũng
158
+ về cùng một format.
159
+ - Thêm JSON sink cho structured logging, bật bằng `QUAPP_LOG_FORMAT=json|both`
160
+ (default `text`, giữ nguyên hành vi cũ). Field: `jobId`, `traceId`, `phase`,
161
+ `category`, `errorCode`, `sdk`, `provider`, `device`, `event`, `durationMs`.
162
+ - Thêm redaction filter: mask `token` / `apiKey` / `authentication` /
163
+ `clientSecret` / … trước khi ghi log, áp dụng cả cho stack trace.
164
+ - Fix: text formatter thiếu `{exception}`. Với formatter dạng callable, loguru
165
+ **không** tự append traceback — nên trước đây mọi `logger.exception()` trong
166
+ codebase không in ra stack trace nào.
167
+ - Fix: `depth` trong intercept khiến mọi record hiện là
168
+ `logging:callHandlers:<line>` thay vì file:line của nơi gọi log.
169
+ - Fix: với `enqueue=True`, traceback object không picklable nên bị thay bằng
170
+ `None` sau khi qua queue. Traceback nay được format sẵn ở filter.
171
+
172
+ **Error từ runner không phải Python (`component/dispatcher.py`, `bridge.py`)**
173
+ - Bỏ `subprocess.run(..., check=True)`. Nó raise `CalledProcessError` **trước
174
+ khi** đọc được output, làm nhánh `_extract_error_message()` thành dead code và
175
+ user chỉ thấy `Command '[...]' returned non-zero exit status 1`.
176
+ - Thêm `HandlerRuntimeError` mang `language`, `action`, `error_type`, `stack`
177
+ (stack của runtime gốc — JS/.NET), `exit_code`, `signal_name`. Sáu tình huống
178
+ được phân biệt: runner không có trong image, timeout, bị signal (handler.c
179
+ segfault → returncode âm, stdout rỗng), structured error trên stdout, exit
180
+ code khác 0, và output không phải JSON hợp lệ.
181
+ - Đọc được **cả contract cũ** (`error` dạng string, stack trên stderr) nên không
182
+ cần deploy đồng thời với template.
183
+ - `_format_exception()` đưa `handlerStackTrace` / `language` / `action` /
184
+ `exitCode` / `signal` lên `job_result` cho lỗi handler JS/Q#/C.
185
+
186
+ **Đường lỗi (`model/device/device.py`, `util/response_utils.py`, …)**
187
+ - `Device._on_execution` dùng `build_error_job_response()` thay cho
188
+ `job_result = {"error": str(exception)}` và log ở `ERROR` thay vì `DEBUG`.
189
+ Đây là đường lỗi quan trọng nhất (submit circuit lên device) và trước đây là
190
+ đường mất thông tin nhiều nhất.
191
+ - `build_error_job_response()` nay tự phát một log `ERROR` duy nhất cho mọi lỗi,
192
+ nên Job Log và Function Log không còn lệch nhau.
193
+ - Truyền `stage` cho 8 call-site còn thiếu (trước đây 10/13 call-site rơi vào
194
+ default `'while running your job'`).
195
+ - Fix `generate_response()`: nhánh `else` truy cập `job_response.status_code`
196
+ khi `job_response` là falsy → `AttributeError`, thay lỗi gốc bằng lỗi khác.
197
+ - Fix `job_fetching.py`: `__produce_histogram_data` / `__get_execution_time`
198
+ khai báo `@staticmethod` nhưng dùng `self.logger` → `NameError` khi được gọi.
199
+ - `DeviceSelection`: không log nguyên response (chứa token provider) và không
200
+ nhét `response.content` thô vào exception (nội dung này chảy vào `job_result`
201
+ rồi hiện lên UI).
202
+ - `PostProcessingTask`: bỏ `print()` ghi thẳng ra stdout (không qua sink nên
203
+ không được redact và không có `job_id`).
204
+
205
+ **Test**
206
+ - `tests/test_logging_config.py` — redaction, idempotent sink, job context,
207
+ intercept stdlib, exception payload.
208
+ - `tests/test_dispatcher_runner_error.py` — 6 nhánh phân loại lỗi runner.
209
+
76
210
  ### v0.0.13.dev2
77
211
  - Fix: `_openqasm_to_braket` in `circuit_adapter.py` now injects a default
78
212
  `Probability` result type when the Braket circuit produced by `tk_to_braket`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.13.dev8"
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" }]
@@ -2,7 +2,6 @@
2
2
  # async_invocation_task.py
3
3
  # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
4
  import asyncio
5
- import sys
6
5
 
7
6
  from starlette.concurrency import run_in_threadpool
8
7
  from starlette.responses import JSONResponse
@@ -10,6 +9,7 @@ from starlette.responses import JSONResponse
10
9
  from ..async_tasks.async_task import AsyncTask
11
10
  from ..component.backend.job_manager import JobManager
12
11
  from ..component.callback.update_job_metadata import update_job_metadata
12
+ from ..config.log_context import bind_job
13
13
  from ..config.logging_config import logger
14
14
  from ..factory.handler_factory import HandlerFactory
15
15
  from ..model.invocation import Event, Error, Success
@@ -61,9 +61,12 @@ class AsyncInvocationTask(AsyncTask):
61
61
  job_id = self.event.json().get('jobId')
62
62
  if job_id is None:
63
63
  raise Exception("Job ID is missing from event")
64
- logger.add(sink=sys.stderr,
65
- format="[ConsoleJobLog][" + job_id + "] " + "{level} : {time} : {message}: {process}",
66
- level='DEBUG')
64
+
65
+ # Trước đây chỗ này gọi logger.add(sink=sys.stderr, ...) cho MỖI
66
+ # request: sau N job có N+1 sink và mỗi dòng log in N+1 lần. Sink đã
67
+ # được cấu hình một lần trong logging_config; ở đây chỉ cần bind
68
+ # context để mọi log sau đó tự có job_id/trace_id.
69
+ job_log = bind_job(job_id)
67
70
 
68
71
  # Define the blocking work
69
72
  def _run_handle():
@@ -78,7 +81,7 @@ class AsyncInvocationTask(AsyncTask):
78
81
  if t.cancelled() or t.exception() is None:
79
82
  return
80
83
  exc = t.exception()
81
- logger.exception(
84
+ job_log.bind(event='background_task_failed').exception(
82
85
  f"Background job task failed for job {job_id}: {exc}")
83
86
  try:
84
87
  event_data = self.event.json()
@@ -87,7 +90,8 @@ class AsyncInvocationTask(AsyncTask):
87
90
  if on_error_url:
88
91
  error_response = build_error_job_response(
89
92
  exc,
90
- message=f"Unhandled background task error: {exc}")
93
+ message=f"Unhandled background task error: {exc}",
94
+ stage='while running your job in the background')
91
95
  error_response.user_token = event_data.get('userToken')
92
96
  error_response.user_identity = event_data.get(
93
97
  'userIdentity')
@@ -97,7 +101,7 @@ class AsyncInvocationTask(AsyncTask):
97
101
  event_data, 'workspaceHeader')
98
102
  update_job_metadata(error_response, on_error_url)
99
103
  except Exception as fallback_exc:
100
- logger.exception(
104
+ job_log.bind(event='callback_failed').exception(
101
105
  f"Failed to notify BE of error for job {job_id}: {fallback_exc}")
102
106
 
103
107
  task.add_done_callback(_on_task_done)
@@ -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
 
@@ -35,15 +36,23 @@ class PostProcessingTask(AsyncTask):
35
36
  job_result_post_processing = self.post_processing_fn(
36
37
  self.promise.job_result)
37
38
 
38
- self.logger.debug("Parsing job result....")
39
- print(job_result_post_processing)
39
+ # print() cũ ghi thẳng ra stdout, không qua sink nên không được
40
+ # redact và không có job_id.
41
+ self.logger.debug(
42
+ f"Parsing job result: type={type(job_result_post_processing).__name__}")
40
43
  job_response.job_result = parse(job_result_post_processing)
41
44
 
42
45
  update_job_metadata(job_response, self.promise.callback_url.on_done)
43
46
 
44
47
  except Exception as exception:
45
- job_response = build_error_job_response(exception, job_response,
46
- message='Error when post processing job result')
48
+ self.logger.bind(event='job_failed').exception(
49
+ f"Post-processing failed: {exception}")
50
+
51
+ job_response = build_error_job_response(
52
+ exception, job_response,
53
+ message='Error when post processing job result',
54
+ stage='while post-processing the job result',
55
+ phase=RuntimePhase.POST_PROCESSING)
47
56
 
48
57
  update_job_metadata(job_response,
49
58
  self.promise.callback_url.on_error)
@@ -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
- self.logger.debug("Preparing backend data")
86
- self.__prepare_backend_data(circuit)
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
- provider = self._create_provider()
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
- self.logger.debug(f"Executing job with device name: {device_name}")
131
- device = self._create_device(provider)
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
- circuit = circuit_preparation_fn(self.input)
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)
@@ -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
@@ -131,9 +132,12 @@ class JobFetcher(ABC):
131
132
  f"Exception during job fetch for provider_job_id {self.provider_job_id}: {exception}",
132
133
  exc_info=True)
133
134
 
134
- job_response = build_error_job_response(exception, job_response,
135
- message='Error when fetching job with provider_job_id {0}'.format(
136
- self.provider_job_id))
135
+ job_response = build_error_job_response(
136
+ exception, job_response,
137
+ message='Error when fetching job with provider_job_id {0}'.format(
138
+ self.provider_job_id),
139
+ stage='while fetching the job result from the provider',
140
+ phase=RuntimePhase.POLLING)
137
141
  update_job_metadata(job_response, self.callback_urls[
138
142
  InvocationStep.EXECUTION].on_error)
139
143
 
@@ -208,8 +212,11 @@ class JobFetcher(ABC):
208
212
  self.logger.exception(f"Exception during analysis: {exception}",
209
213
  exc_info=True)
210
214
  from ...util.response_utils import build_error_job_response
211
- job_response = build_error_job_response(exception, job_response,
212
- message='Error when analyzing job result')
215
+ job_response = build_error_job_response(
216
+ exception, job_response,
217
+ message='Error when analyzing job result',
218
+ stage='while analysing the job result',
219
+ phase=RuntimePhase.ANALYSIS)
213
220
 
214
221
  update_job_metadata(job_response, callback_url.on_error)
215
222
  return None
@@ -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,13 +90,16 @@ class JobFetching(ABC):
89
90
  job_response.status_code = StatusCode.POLLING
90
91
 
91
92
  except Exception as exception:
92
- self.logger.debug(
93
+ self.logger.bind(event='job_failed').exception(
93
94
  "Exception when fetch job with provider_job_id {0}: {1}".format(
94
95
  self.provider_job_id, str(exception)))
95
96
 
96
- job_response = build_error_job_response(exception, job_response,
97
- message='Exception when fetch job with provider_job_id {0}'.format(
98
- self.provider_job_id))
97
+ job_response = build_error_job_response(
98
+ exception, job_response,
99
+ message='Exception when fetch job with provider_job_id {0}'.format(
100
+ self.provider_job_id),
101
+ stage='while fetching the job result from the provider',
102
+ phase=RuntimePhase.POLLING)
99
103
 
100
104
  update_job_metadata(job_response, self.callback_dict.get(
101
105
  InvocationStep.EXECUTION).on_error)
@@ -156,9 +160,12 @@ class JobFetching(ABC):
156
160
  "Exception when analyst job result with provider_job_id {0}: {1}".format(
157
161
  self.provider_job_id, str(exception)))
158
162
 
159
- job_response = build_error_job_response(exception, job_response,
160
- message='Exception when analyst job result with provider_job_id {0}'.format(
161
- self.provider_job_id))
163
+ job_response = build_error_job_response(
164
+ exception, job_response,
165
+ message='Exception when analyst job result with provider_job_id {0}'.format(
166
+ self.provider_job_id),
167
+ stage='while analysing the job result',
168
+ phase=RuntimePhase.ANALYSIS)
162
169
 
163
170
  update_job_metadata(job_response, callback_url.on_error)
164
171
 
@@ -183,8 +190,10 @@ class JobFetching(ABC):
183
190
  PostProcessingTask(post_processing_fn=post_processing_fn,
184
191
  promise=promise).do()
185
192
 
186
- @staticmethod
187
- def __produce_histogram_data(job_result) -> Any | None:
193
+ # Trước đây hai method dưới là @staticmethod nhưng vẫn dùng `self.logger`,
194
+ # nên chúng raise NameError ngay khi được gọi -- biến lỗi analysis thành một
195
+ # lỗi vô nghĩa hoàn toàn khác. Bỏ @staticmethod để `self` hợp lệ.
196
+ def __produce_histogram_data(self, job_result) -> Any | None:
188
197
  """
189
198
 
190
199
  @param job_result:
@@ -196,14 +205,13 @@ class JobFetching(ABC):
196
205
  histogram_data = job_result.result()[0].data.meas.get_counts()
197
206
 
198
207
  except Exception as e:
199
- self.logger.debug(
208
+ self.logger.warning(
200
209
  "Can't produce histogram with error: {0}".format(str(e)))
201
210
  histogram_data = None
202
211
 
203
212
  return histogram_data
204
213
 
205
- @staticmethod
206
- def __get_execution_time(job_result):
214
+ def __get_execution_time(self, job_result):
207
215
  """
208
216
 
209
217
  @param job_result: