quapp-common 0.0.13.dev7__tar.gz → 0.0.13.dev9__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 (97) hide show
  1. {quapp_common-0.0.13.dev7/quapp_common.egg-info → quapp_common-0.0.13.dev9}/PKG-INFO +73 -1
  2. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/README.md +72 -0
  3. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/pyproject.toml +1 -1
  4. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/async_invocation_task.py +11 -7
  5. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/post_processing_task.py +11 -4
  6. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_fetcher.py +9 -5
  7. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_fetching.py +17 -12
  8. quapp_common-0.0.13.dev9/quapp_common/component/backend/job_heartbeat.py +83 -0
  9. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_manager.py +6 -0
  10. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/bridge.py +18 -20
  11. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/device/device_selection.py +15 -5
  12. quapp_common-0.0.13.dev9/quapp_common/component/dispatcher.py +272 -0
  13. quapp_common-0.0.13.dev9/quapp_common/config/log_context.py +102 -0
  14. quapp_common-0.0.13.dev9/quapp_common/config/logging_config.py +276 -0
  15. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/handler/handler.py +6 -1
  16. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/device/custom_device.py +4 -2
  17. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/device/device.py +16 -8
  18. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/response_utils.py +58 -17
  19. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9/quapp_common.egg-info}/PKG-INFO +73 -1
  20. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/SOURCES.txt +5 -0
  21. quapp_common-0.0.13.dev9/tests/test_dispatcher_runner_error.py +196 -0
  22. quapp_common-0.0.13.dev9/tests/test_job_heartbeat.py +115 -0
  23. quapp_common-0.0.13.dev9/tests/test_logging_config.py +262 -0
  24. quapp_common-0.0.13.dev7/quapp_common/component/dispatcher.py +0 -123
  25. quapp_common-0.0.13.dev7/quapp_common/config/logging_config.py +0 -33
  26. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/LICENSE +0 -0
  27. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/__init__.py +0 -0
  28. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/__init__.py +0 -0
  29. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/async_task.py +0 -0
  30. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  31. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/__init__.py +0 -0
  32. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/__init__.py +0 -0
  33. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/invocation.py +0 -0
  34. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/callback/__init__.py +0 -0
  35. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/callback/update_job_metadata.py +0 -0
  36. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/circuit_adapter.py +0 -0
  37. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/device/__init__.py +0 -0
  38. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/result_serializer.py +0 -0
  39. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/config/__init__.py +0 -0
  40. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/config/thread_config.py +0 -0
  41. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/__init__.py +0 -0
  42. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/__init__.py +0 -0
  43. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  44. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  45. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  46. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/backend/__init__.py +0 -0
  47. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/backend/backend_information.py +0 -0
  48. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/callback/__init__.py +0 -0
  49. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/callback/callback_url.py +0 -0
  50. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/device/__init__.py +0 -0
  51. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/device/circuit_running_option.py +0 -0
  52. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/promise/__init__.py +0 -0
  53. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/promise/post_processing_promise.py +0 -0
  54. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/promise/promise.py +0 -0
  55. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/__init__.py +0 -0
  56. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/invocation_request.py +0 -0
  57. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/job_fetching_request.py +0 -0
  58. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/request.py +0 -0
  59. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/__init__.py +0 -0
  60. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/authentication.py +0 -0
  61. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/custom_header.py +0 -0
  62. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/job_response.py +0 -0
  63. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/__init__.py +0 -0
  64. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/base_enum.py +0 -0
  65. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/http_header.py +0 -0
  66. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/invocation_step.py +0 -0
  67. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/language.py +0 -0
  68. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/media_type.py +0 -0
  69. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/processing_unit.py +0 -0
  70. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/provider_tag.py +0 -0
  71. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/sdk.py +0 -0
  72. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/status/__init__.py +0 -0
  73. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/status/job_status.py +0 -0
  74. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/status/status_code.py +0 -0
  75. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/token_type.py +0 -0
  76. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/__init__.py +0 -0
  77. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/device_factory.py +0 -0
  78. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/handler_factory.py +0 -0
  79. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/provider_factory.py +0 -0
  80. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/handler/__init__.py +0 -0
  81. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/__init__.py +0 -0
  82. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/device/__init__.py +0 -0
  83. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/invocation.py +0 -0
  84. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/provider/__init__.py +0 -0
  85. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/provider/provider.py +0 -0
  86. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/__init__.py +0 -0
  87. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/file_utils.py +0 -0
  88. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/http_utils.py +0 -0
  89. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/json_parser_utils.py +0 -0
  90. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/dependency_links.txt +0 -0
  91. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/requires.txt +0 -0
  92. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/top_level.txt +0 -0
  93. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/setup.cfg +0 -0
  94. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/tests/test_circuit_adapter.py +0 -0
  95. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/tests/test_json_parser_utils.py +0 -0
  96. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/tests/test_language_dispatcher.py +0 -0
  97. {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/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.dev7
3
+ Version: 0.0.13.dev9
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,78 @@ Notes:
114
114
 
115
115
  ## Recently Changes Highlights
116
116
 
117
+ ### v0.0.14.dev1 — Error logging & observability (Phase 0)
118
+
119
+ 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
+ đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
121
+ không đổi schema `job_result`, không cần frontend/backend sửa gì.
122
+
123
+ **Logging (`config/`)**
124
+ - Thêm `config/log_context.py`: `bind_job()` / `enrich_job_context()` đặt
125
+ `job_id`, `trace_id`, `sdk`, `provider`, `device` vào `contextvars`. Mọi log
126
+ sau đó tự có đủ field, kể cả log của `Provider` / `DeviceSelection` /
127
+ `*Factory` — những chỗ trước đây rơi vào context `"QuappLibs"` nên không gắn
128
+ được vào job nào.
129
+ - `configure_logging()` **idempotent**. Trước đây `AsyncInvocationTask.do()` gọi
130
+ `logger.add(sink=sys.stderr, ...)` cho mỗi request, nên sau N job có N+1 sink
131
+ và mỗi dòng log in N+1 lần.
132
+ - Thêm `InterceptHandler` + `install_stdlib_intercept()`: đẩy mọi record của
133
+ stdlib `logging` sang loguru. Cần thiết vì 24/34 template trong
134
+ `quapp-sdk-templates` dùng `logging.getLogger('index')` mà không cấu hình
135
+ handler — `debug`/`info` của chúng bị drop im lặng và `exception` in ra không
136
+ có prefix `[ConsoleJobLog]` lẫn `job_id`. Log của uvicorn/qiskit/botocore cũng
137
+ về cùng một format.
138
+ - Thêm JSON sink cho structured logging, bật bằng `QUAPP_LOG_FORMAT=json|both`
139
+ (default `text`, giữ nguyên hành vi cũ). Field: `jobId`, `traceId`, `phase`,
140
+ `category`, `errorCode`, `sdk`, `provider`, `device`, `event`, `durationMs`.
141
+ - Thêm redaction filter: mask `token` / `apiKey` / `authentication` /
142
+ `clientSecret` / … trước khi ghi log, áp dụng cả cho stack trace.
143
+ - Fix: text formatter thiếu `{exception}`. Với formatter dạng callable, loguru
144
+ **không** tự append traceback — nên trước đây mọi `logger.exception()` trong
145
+ codebase không in ra stack trace nào.
146
+ - Fix: `depth` trong intercept khiến mọi record hiện là
147
+ `logging:callHandlers:<line>` thay vì file:line của nơi gọi log.
148
+ - Fix: với `enqueue=True`, traceback object không picklable nên bị thay bằng
149
+ `None` sau khi qua queue. Traceback nay được format sẵn ở filter.
150
+
151
+ **Error từ runner không phải Python (`component/dispatcher.py`, `bridge.py`)**
152
+ - Bỏ `subprocess.run(..., check=True)`. Nó raise `CalledProcessError` **trước
153
+ khi** đọc được output, làm nhánh `_extract_error_message()` thành dead code và
154
+ user chỉ thấy `Command '[...]' returned non-zero exit status 1`.
155
+ - Thêm `HandlerRuntimeError` mang `language`, `action`, `error_type`, `stack`
156
+ (stack của runtime gốc — JS/.NET), `exit_code`, `signal_name`. Sáu tình huống
157
+ được phân biệt: runner không có trong image, timeout, bị signal (handler.c
158
+ segfault → returncode âm, stdout rỗng), structured error trên stdout, exit
159
+ code khác 0, và output không phải JSON hợp lệ.
160
+ - Đọc được **cả contract cũ** (`error` dạng string, stack trên stderr) nên không
161
+ cần deploy đồng thời với template.
162
+ - `_format_exception()` đưa `handlerStackTrace` / `language` / `action` /
163
+ `exitCode` / `signal` lên `job_result` cho lỗi handler JS/Q#/C.
164
+
165
+ **Đường lỗi (`model/device/device.py`, `util/response_utils.py`, …)**
166
+ - `Device._on_execution` dùng `build_error_job_response()` thay cho
167
+ `job_result = {"error": str(exception)}` và log ở `ERROR` thay vì `DEBUG`.
168
+ Đây là đường lỗi quan trọng nhất (submit circuit lên device) và trước đây là
169
+ đường mất thông tin nhiều nhất.
170
+ - `build_error_job_response()` nay tự phát một log `ERROR` duy nhất cho mọi lỗi,
171
+ nên Job Log và Function Log không còn lệch nhau.
172
+ - Truyền `stage` cho 8 call-site còn thiếu (trước đây 10/13 call-site rơi vào
173
+ default `'while running your job'`).
174
+ - Fix `generate_response()`: nhánh `else` truy cập `job_response.status_code`
175
+ khi `job_response` là falsy → `AttributeError`, thay lỗi gốc bằng lỗi khác.
176
+ - Fix `job_fetching.py`: `__produce_histogram_data` / `__get_execution_time`
177
+ khai báo `@staticmethod` nhưng dùng `self.logger` → `NameError` khi được gọi.
178
+ - `DeviceSelection`: không log nguyên response (chứa token provider) và không
179
+ nhét `response.content` thô vào exception (nội dung này chảy vào `job_result`
180
+ rồi hiện lên UI).
181
+ - `PostProcessingTask`: bỏ `print()` ghi thẳng ra stdout (không qua sink nên
182
+ không được redact và không có `job_id`).
183
+
184
+ **Test**
185
+ - `tests/test_logging_config.py` — redaction, idempotent sink, job context,
186
+ intercept stdlib, exception payload.
187
+ - `tests/test_dispatcher_runner_error.py` — 6 nhánh phân loại lỗi runner.
188
+
117
189
  ### v0.0.13.dev2
118
190
  - Fix: `_openqasm_to_braket` in `circuit_adapter.py` now injects a default
119
191
  `Probability` result type when the Braket circuit produced by `tk_to_braket`
@@ -73,6 +73,78 @@ Notes:
73
73
 
74
74
  ## Recently Changes Highlights
75
75
 
76
+ ### v0.0.14.dev1 — Error logging & observability (Phase 0)
77
+
78
+ 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
+ đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
80
+ không đổi schema `job_result`, không cần frontend/backend sửa gì.
81
+
82
+ **Logging (`config/`)**
83
+ - Thêm `config/log_context.py`: `bind_job()` / `enrich_job_context()` đặt
84
+ `job_id`, `trace_id`, `sdk`, `provider`, `device` vào `contextvars`. Mọi log
85
+ sau đó tự có đủ field, kể cả log của `Provider` / `DeviceSelection` /
86
+ `*Factory` — những chỗ trước đây rơi vào context `"QuappLibs"` nên không gắn
87
+ được vào job nào.
88
+ - `configure_logging()` **idempotent**. Trước đây `AsyncInvocationTask.do()` gọi
89
+ `logger.add(sink=sys.stderr, ...)` cho mỗi request, nên sau N job có N+1 sink
90
+ và mỗi dòng log in N+1 lần.
91
+ - Thêm `InterceptHandler` + `install_stdlib_intercept()`: đẩy mọi record của
92
+ stdlib `logging` sang loguru. Cần thiết vì 24/34 template trong
93
+ `quapp-sdk-templates` dùng `logging.getLogger('index')` mà không cấu hình
94
+ handler — `debug`/`info` của chúng bị drop im lặng và `exception` in ra không
95
+ có prefix `[ConsoleJobLog]` lẫn `job_id`. Log của uvicorn/qiskit/botocore cũng
96
+ về cùng một format.
97
+ - Thêm JSON sink cho structured logging, bật bằng `QUAPP_LOG_FORMAT=json|both`
98
+ (default `text`, giữ nguyên hành vi cũ). Field: `jobId`, `traceId`, `phase`,
99
+ `category`, `errorCode`, `sdk`, `provider`, `device`, `event`, `durationMs`.
100
+ - Thêm redaction filter: mask `token` / `apiKey` / `authentication` /
101
+ `clientSecret` / … trước khi ghi log, áp dụng cả cho stack trace.
102
+ - Fix: text formatter thiếu `{exception}`. Với formatter dạng callable, loguru
103
+ **không** tự append traceback — nên trước đây mọi `logger.exception()` trong
104
+ codebase không in ra stack trace nào.
105
+ - Fix: `depth` trong intercept khiến mọi record hiện là
106
+ `logging:callHandlers:<line>` thay vì file:line của nơi gọi log.
107
+ - Fix: với `enqueue=True`, traceback object không picklable nên bị thay bằng
108
+ `None` sau khi qua queue. Traceback nay được format sẵn ở filter.
109
+
110
+ **Error từ runner không phải Python (`component/dispatcher.py`, `bridge.py`)**
111
+ - Bỏ `subprocess.run(..., check=True)`. Nó raise `CalledProcessError` **trước
112
+ khi** đọc được output, làm nhánh `_extract_error_message()` thành dead code và
113
+ user chỉ thấy `Command '[...]' returned non-zero exit status 1`.
114
+ - Thêm `HandlerRuntimeError` mang `language`, `action`, `error_type`, `stack`
115
+ (stack của runtime gốc — JS/.NET), `exit_code`, `signal_name`. Sáu tình huống
116
+ được phân biệt: runner không có trong image, timeout, bị signal (handler.c
117
+ segfault → returncode âm, stdout rỗng), structured error trên stdout, exit
118
+ code khác 0, và output không phải JSON hợp lệ.
119
+ - Đọc được **cả contract cũ** (`error` dạng string, stack trên stderr) nên không
120
+ cần deploy đồng thời với template.
121
+ - `_format_exception()` đưa `handlerStackTrace` / `language` / `action` /
122
+ `exitCode` / `signal` lên `job_result` cho lỗi handler JS/Q#/C.
123
+
124
+ **Đường lỗi (`model/device/device.py`, `util/response_utils.py`, …)**
125
+ - `Device._on_execution` dùng `build_error_job_response()` thay cho
126
+ `job_result = {"error": str(exception)}` và log ở `ERROR` thay vì `DEBUG`.
127
+ Đây là đường lỗi quan trọng nhất (submit circuit lên device) và trước đây là
128
+ đường mất thông tin nhiều nhất.
129
+ - `build_error_job_response()` nay tự phát một log `ERROR` duy nhất cho mọi lỗi,
130
+ nên Job Log và Function Log không còn lệch nhau.
131
+ - Truyền `stage` cho 8 call-site còn thiếu (trước đây 10/13 call-site rơi vào
132
+ default `'while running your job'`).
133
+ - Fix `generate_response()`: nhánh `else` truy cập `job_response.status_code`
134
+ khi `job_response` là falsy → `AttributeError`, thay lỗi gốc bằng lỗi khác.
135
+ - Fix `job_fetching.py`: `__produce_histogram_data` / `__get_execution_time`
136
+ khai báo `@staticmethod` nhưng dùng `self.logger` → `NameError` khi được gọi.
137
+ - `DeviceSelection`: không log nguyên response (chứa token provider) và không
138
+ nhét `response.content` thô vào exception (nội dung này chảy vào `job_result`
139
+ rồi hiện lên UI).
140
+ - `PostProcessingTask`: bỏ `print()` ghi thẳng ra stdout (không qua sink nên
141
+ không được redact và không có `job_id`).
142
+
143
+ **Test**
144
+ - `tests/test_logging_config.py` — redaction, idempotent sink, job context,
145
+ intercept stdlib, exception payload.
146
+ - `tests/test_dispatcher_runner_error.py` — 6 nhánh phân loại lỗi runner.
147
+
76
148
  ### v0.0.13.dev2
77
149
  - Fix: `_openqasm_to_braket` in `circuit_adapter.py` now injects a default
78
150
  `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.dev7"
7
+ version = "0.0.13.dev9"
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)
@@ -35,15 +35,22 @@ class PostProcessingTask(AsyncTask):
35
35
  job_result_post_processing = self.post_processing_fn(
36
36
  self.promise.job_result)
37
37
 
38
- self.logger.debug("Parsing job result....")
39
- print(job_result_post_processing)
38
+ # print() ghi thẳng ra stdout, không qua sink nên không được
39
+ # redact và không có job_id.
40
+ self.logger.debug(
41
+ f"Parsing job result: type={type(job_result_post_processing).__name__}")
40
42
  job_response.job_result = parse(job_result_post_processing)
41
43
 
42
44
  update_job_metadata(job_response, self.promise.callback_url.on_done)
43
45
 
44
46
  except Exception as exception:
45
- job_response = build_error_job_response(exception, job_response,
46
- message='Error when post processing job result')
47
+ self.logger.bind(event='job_failed').exception(
48
+ f"Post-processing failed: {exception}")
49
+
50
+ job_response = build_error_job_response(
51
+ exception, job_response,
52
+ message='Error when post processing job result',
53
+ stage='while post-processing the job result')
47
54
 
48
55
  update_job_metadata(job_response,
49
56
  self.promise.callback_url.on_error)
@@ -131,9 +131,11 @@ class JobFetcher(ABC):
131
131
  f"Exception during job fetch for provider_job_id {self.provider_job_id}: {exception}",
132
132
  exc_info=True)
133
133
 
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))
134
+ job_response = build_error_job_response(
135
+ exception, job_response,
136
+ message='Error when fetching job with provider_job_id {0}'.format(
137
+ self.provider_job_id),
138
+ stage='while fetching the job result from the provider')
137
139
  update_job_metadata(job_response, self.callback_urls[
138
140
  InvocationStep.EXECUTION].on_error)
139
141
 
@@ -208,8 +210,10 @@ class JobFetcher(ABC):
208
210
  self.logger.exception(f"Exception during analysis: {exception}",
209
211
  exc_info=True)
210
212
  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')
213
+ job_response = build_error_job_response(
214
+ exception, job_response,
215
+ message='Error when analyzing job result',
216
+ stage='while analysing the job result')
213
217
 
214
218
  update_job_metadata(job_response, callback_url.on_error)
215
219
  return None
@@ -89,13 +89,15 @@ class JobFetching(ABC):
89
89
  job_response.status_code = StatusCode.POLLING
90
90
 
91
91
  except Exception as exception:
92
- self.logger.debug(
92
+ self.logger.bind(event='job_failed').exception(
93
93
  "Exception when fetch job with provider_job_id {0}: {1}".format(
94
94
  self.provider_job_id, str(exception)))
95
95
 
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))
96
+ job_response = build_error_job_response(
97
+ exception, job_response,
98
+ message='Exception when fetch job with provider_job_id {0}'.format(
99
+ self.provider_job_id),
100
+ stage='while fetching the job result from the provider')
99
101
 
100
102
  update_job_metadata(job_response, self.callback_dict.get(
101
103
  InvocationStep.EXECUTION).on_error)
@@ -156,9 +158,11 @@ class JobFetching(ABC):
156
158
  "Exception when analyst job result with provider_job_id {0}: {1}".format(
157
159
  self.provider_job_id, str(exception)))
158
160
 
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))
161
+ job_response = build_error_job_response(
162
+ exception, job_response,
163
+ message='Exception when analyst job result with provider_job_id {0}'.format(
164
+ self.provider_job_id),
165
+ stage='while analysing the job result')
162
166
 
163
167
  update_job_metadata(job_response, callback_url.on_error)
164
168
 
@@ -183,8 +187,10 @@ class JobFetching(ABC):
183
187
  PostProcessingTask(post_processing_fn=post_processing_fn,
184
188
  promise=promise).do()
185
189
 
186
- @staticmethod
187
- def __produce_histogram_data(job_result) -> Any | None:
190
+ # Trước đây hai method dưới là @staticmethod nhưng vẫn dùng `self.logger`,
191
+ # nên chúng raise NameError ngay khi được gọi -- biến lỗi analysis thành một
192
+ # lỗi vô nghĩa hoàn toàn khác. Bỏ @staticmethod để `self` hợp lệ.
193
+ def __produce_histogram_data(self, job_result) -> Any | None:
188
194
  """
189
195
 
190
196
  @param job_result:
@@ -196,14 +202,13 @@ class JobFetching(ABC):
196
202
  histogram_data = job_result.result()[0].data.meas.get_counts()
197
203
 
198
204
  except Exception as e:
199
- self.logger.debug(
205
+ self.logger.warning(
200
206
  "Can't produce histogram with error: {0}".format(str(e)))
201
207
  histogram_data = None
202
208
 
203
209
  return histogram_data
204
210
 
205
- @staticmethod
206
- def __get_execution_time(job_result):
211
+ def __get_execution_time(self, job_result):
207
212
  """
208
213
 
209
214
  @param job_result:
@@ -0,0 +1,83 @@
1
+ # Quapp Platform Project
2
+ # job_heartbeat.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ import os
6
+ import threading
7
+
8
+ import requests
9
+
10
+ from ...config.logging_config import logger
11
+
12
+ DEFAULT_INTERVAL_SEC = 30.0
13
+ REQUEST_TIMEOUT_SEC = 5.0
14
+ TERMINAL_STATUSES = ('DONE', 'FAILED')
15
+
16
+ _lock = threading.Lock()
17
+ _thread = None
18
+ _stop_event = threading.Event()
19
+
20
+
21
+ def heartbeat_interval_sec() -> float:
22
+ raw = os.getenv('HEARTBEAT_INTERVAL_SEC', '')
23
+ try:
24
+ value = float(raw)
25
+ if value > 0:
26
+ return value
27
+ except ValueError:
28
+ pass
29
+ return DEFAULT_INTERVAL_SEC
30
+
31
+
32
+ def beat_once() -> int:
33
+ from .job_manager import JobManager, META, STATUS, SCHEDULER_CALLBACK_URL
34
+
35
+ sent = 0
36
+ for job_id, data in JobManager.list_jobs().items():
37
+ if data.get(STATUS) in TERMINAL_STATUSES:
38
+ continue
39
+ url = (data.get(META) or {}).get(SCHEDULER_CALLBACK_URL)
40
+ if not url:
41
+ continue
42
+ try:
43
+ response = requests.patch(
44
+ url, json={'status': 'RUNNING', 'heartbeat': True},
45
+ timeout=REQUEST_TIMEOUT_SEC)
46
+ logger.debug(
47
+ f'Heartbeat for job {job_id} returned {response.status_code}')
48
+ sent += 1
49
+ except Exception as exception:
50
+ logger.debug(f'Heartbeat for job {job_id} failed: {exception}')
51
+ return sent
52
+
53
+
54
+ def _run() -> None:
55
+ while True:
56
+ try:
57
+ beat_once()
58
+ except Exception as exception:
59
+ logger.exception(f'Heartbeat loop error: {exception}')
60
+ if _stop_event.wait(heartbeat_interval_sec()):
61
+ return
62
+
63
+
64
+ def ensure_started() -> None:
65
+ global _thread
66
+ with _lock:
67
+ if _thread is not None and _thread.is_alive():
68
+ return
69
+ _stop_event.clear()
70
+ _thread = threading.Thread(target=_run, name='job-heartbeat',
71
+ daemon=True)
72
+ _thread.start()
73
+ logger.info(
74
+ f'Job heartbeat started with interval {heartbeat_interval_sec()}s')
75
+
76
+
77
+ def stop() -> None:
78
+ global _thread
79
+ with _lock:
80
+ _stop_event.set()
81
+ if _thread is not None:
82
+ _thread.join(timeout=1.0)
83
+ _thread = None
@@ -66,6 +66,9 @@ class JobManager:
66
66
  """
67
67
  Stop any listener threads in this process and shutdown Manager.
68
68
  """
69
+ from .job_heartbeat import stop
70
+ stop()
71
+
69
72
  # stop listeners
70
73
  for key, (t, stop_ev) in list(cls._listener_threads.items()):
71
74
  stop_ev.set()
@@ -100,6 +103,9 @@ class JobManager:
100
103
  cls._jobs[job_id] = data # proxy is process-safe
101
104
  cls._publish(job_id, {"event": "added", **data})
102
105
 
106
+ from .job_heartbeat import ensure_started
107
+ ensure_started()
108
+
103
109
  @classmethod
104
110
  def update_status(cls, job_id: str, status: str,
105
111
  meta_update: Optional[Dict[str, Any]] = None) -> None:
@@ -4,7 +4,7 @@
4
4
  from typing import Any, Dict
5
5
 
6
6
  from .circuit_adapter import adapt
7
- from .dispatcher import dispatch
7
+ from .dispatcher import HandlerRuntimeError, dispatch
8
8
  from .result_serializer import serialize
9
9
  from ..config.logging_config import logger
10
10
  from ..enum.language import Language
@@ -47,25 +47,27 @@ class SubprocessBridge:
47
47
  logger.info(f"SubprocessBridge.processing: dispatching to {self.language.value} | sdk={self.sdk}")
48
48
  logger.debug(f"SubprocessBridge.processing: input_keys={list(invocation_input.keys())}")
49
49
 
50
+ # dispatch() đã raise HandlerRuntimeError cho mọi trường hợp lỗi
51
+ # (structured error, exit code khác 0, signal, output không hợp lệ) với
52
+ # đầy đủ error_type / stack của runner, nên ở đây không cần kiểm
53
+ # status == "error" nữa.
50
54
  result = dispatch(self.language, {
51
55
  "action": "processing",
52
56
  "input": invocation_input
53
57
  })
54
58
 
55
- status = result.get("status")
56
- logger.debug(f"SubprocessBridge.processing: JS status={status}")
57
-
58
- if status == "error":
59
- logger.error(f"SubprocessBridge.processing: JS error={result.get('error')}")
60
- raise RuntimeError(f"JS processing failed: {result.get('error')}")
61
-
62
59
  payload = result.get("result")
63
60
  if payload is None:
64
- raise RuntimeError("JS processing returned no 'result' field")
61
+ raise HandlerRuntimeError(
62
+ language=self.language.value, action='processing',
63
+ error_type='InvalidOutput',
64
+ message="Handler không trả về field 'result'.")
65
65
  if not isinstance(payload, dict):
66
- raise RuntimeError(
67
- f"JS processing returned invalid 'result' type: {type(payload).__name__}"
68
- )
66
+ raise HandlerRuntimeError(
67
+ language=self.language.value, action='processing',
68
+ error_type='InvalidOutput',
69
+ message=f"Field 'result' phải là object, nhận được "
70
+ f"{type(payload).__name__}.")
69
71
 
70
72
  fmt = payload.get("format")
71
73
  logger.debug(f"SubprocessBridge.processing: JS returned format={fmt}; adapting for sdk={self.sdk}")
@@ -98,16 +100,12 @@ class SubprocessBridge:
98
100
  "job_result": serialized
99
101
  })
100
102
 
101
- status = result.get("status")
102
- logger.debug(f"SubprocessBridge.post_processing: JS status={status}")
103
-
104
- if status == "error":
105
- logger.error(f"SubprocessBridge.post_processing: JS error={result.get('error')}")
106
- raise RuntimeError(f"JS post_processing failed: {result.get('error')}")
107
-
108
103
  payload = result.get("result")
109
104
  if payload is None:
110
- raise RuntimeError("JS post_processing returned no 'result' field")
105
+ raise HandlerRuntimeError(
106
+ language=self.language.value, action='post_processing',
107
+ error_type='InvalidOutput',
108
+ message="Handler không trả về field 'result'.")
111
109
 
112
110
  logger.info(f"SubprocessBridge.post_processing: completed successfully | result type={type(payload).__name__}")
113
111
  return payload
@@ -60,21 +60,31 @@ class DeviceSelection:
60
60
  response = requests.get(self.url, params=request, headers=header)
61
61
 
62
62
  if response.status_code != 200:
63
- logger.error(
64
- 'Select device fail with status code: {0} and content: {1}'.format(
65
- response.status_code, response.content))
63
+ logger.bind(event='device_selection_failed',
64
+ status_code=response.status_code).error(
65
+ 'Select device fail with status code: {0}'.format(
66
+ response.status_code))
66
67
 
67
- raise ValueError(f"Select device failed: {response.content}")
68
+ # Không đưa response.content thô vào exception: nội dung này chảy
69
+ # vào job_result rồi hiện lên UI, và có thể mang theo credential.
70
+ raise ValueError(
71
+ f'Select device failed with status code '
72
+ f'{response.status_code}. Xem Function Log để biết chi tiết.')
68
73
 
69
74
  try:
70
75
  response_dict = response.json().get("data")
71
76
 
77
+ # response_dict chứa cả `authentication` = token của provider, nên
78
+ # chỉ log các field không phải secret.
72
79
  logger.debug(
73
- 'Select device successfully: {0}'.format(response_dict))
80
+ 'Select device successfully: providerTag={0}, deviceName={1}'.format(
81
+ response_dict.get("providerTag"),
82
+ response_dict.get("deviceName")))
74
83
 
75
84
  return BackendInformation(provider_tag=ProviderTag.resolve(
76
85
  response_dict.get("providerTag")),
77
86
  device_name=response_dict.get("deviceName"),
78
87
  authentication=response_dict.get("authentication"))
79
88
  except JSONDecodeError:
89
+ logger.warning('Select device response is not valid JSON')
80
90
  return None