quapp-common 0.0.14.dev1__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.14.dev1/quapp_common.egg-info → quapp_common-0.0.14.dev2}/PKG-INFO +51 -1
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/README.md +50 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/pyproject.toml +1 -1
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/post_processing_task.py +0 -3
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/invocation.py +0 -11
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetcher.py +0 -6
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetching.py +0 -8
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/logging_config.py +30 -6
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/custom_device.py +0 -4
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/device.py +0 -3
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2/quapp_common.egg-info}/PKG-INFO +51 -1
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/SOURCES.txt +2 -0
- quapp_common-0.0.14.dev2/tests/test_error_log_dedupe.py +176 -0
- quapp_common-0.0.14.dev2/tests/test_log_secret_leak.py +160 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/LICENSE +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_invocation_task.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/bridge.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/dispatcher.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/log_context.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/thread_config.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/error_report.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/error_category.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/runtime_phase.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/invocation.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/error_classifier.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/invocation_failure.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/response_utils.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/setup.cfg +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_dispatcher_runner_error.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_error_classifier.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_error_report.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_invocation_failure.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_logging_config.py +0 -0
- {quapp_common-0.0.14.dev1 → 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.14.
|
|
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,6 +114,56 @@ Notes:
|
|
|
114
114
|
|
|
115
115
|
## Recently Changes Highlights
|
|
116
116
|
|
|
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
|
+
|
|
117
167
|
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
118
168
|
|
|
119
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
|
|
@@ -73,6 +73,56 @@ Notes:
|
|
|
73
73
|
|
|
74
74
|
## Recently Changes Highlights
|
|
75
75
|
|
|
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
|
+
|
|
76
126
|
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
77
127
|
|
|
78
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quapp-common"
|
|
7
|
-
version = "0.0.14.
|
|
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" }]
|
|
@@ -45,9 +45,6 @@ class PostProcessingTask(AsyncTask):
|
|
|
45
45
|
update_job_metadata(job_response, self.promise.callback_url.on_done)
|
|
46
46
|
|
|
47
47
|
except Exception as exception:
|
|
48
|
-
self.logger.bind(event='job_failed').exception(
|
|
49
|
-
f"Post-processing failed: {exception}")
|
|
50
|
-
|
|
51
48
|
job_response = build_error_job_response(
|
|
52
49
|
exception, job_response,
|
|
53
50
|
message='Error when post processing job result',
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/invocation.py
RENAMED
|
@@ -94,10 +94,6 @@ class Invocation(ABC):
|
|
|
94
94
|
f"Backend data prepared: device {self.backend_information.device_name}, provider {self.backend_information.provider_tag.value}")
|
|
95
95
|
|
|
96
96
|
except Exception as exception:
|
|
97
|
-
self.logger.exception(
|
|
98
|
-
f"Error when prepare backend data: {exception}",
|
|
99
|
-
exc_info=True)
|
|
100
|
-
|
|
101
97
|
job_response = build_error_job_response(exception,
|
|
102
98
|
message='Error when prepare backend data',
|
|
103
99
|
stage='while preparing the backend',
|
|
@@ -143,10 +139,6 @@ class Invocation(ABC):
|
|
|
143
139
|
device = self._create_device(provider)
|
|
144
140
|
|
|
145
141
|
except Exception as exception:
|
|
146
|
-
self.logger.exception(
|
|
147
|
-
f"Exception when create provider or device: {exception}",
|
|
148
|
-
exc_info=True)
|
|
149
|
-
|
|
150
142
|
job_response = build_error_job_response(exception,
|
|
151
143
|
message='Error when create provider or device',
|
|
152
144
|
stage='while running the circuit on the device',
|
|
@@ -199,9 +191,6 @@ class Invocation(ABC):
|
|
|
199
191
|
return circuit
|
|
200
192
|
|
|
201
193
|
except Exception as exception:
|
|
202
|
-
self.logger.exception(f'Error when prepare circuit: {exception}',
|
|
203
|
-
exc_info=True)
|
|
204
|
-
|
|
205
194
|
job_response = build_error_job_response(exception, job_response,
|
|
206
195
|
message='Error when prepare circuit',
|
|
207
196
|
stage='while preparing the circuit',
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetcher.py
RENAMED
|
@@ -128,10 +128,6 @@ class JobFetcher(ABC):
|
|
|
128
128
|
job_response.status_code = StatusCode.POLLING
|
|
129
129
|
|
|
130
130
|
except Exception as exception:
|
|
131
|
-
self.logger.exception(
|
|
132
|
-
f"Exception during job fetch for provider_job_id {self.provider_job_id}: {exception}",
|
|
133
|
-
exc_info=True)
|
|
134
|
-
|
|
135
131
|
job_response = build_error_job_response(
|
|
136
132
|
exception, job_response,
|
|
137
133
|
message='Error when fetching job with provider_job_id {0}'.format(
|
|
@@ -209,8 +205,6 @@ class JobFetcher(ABC):
|
|
|
209
205
|
return job_response
|
|
210
206
|
|
|
211
207
|
except Exception as exception:
|
|
212
|
-
self.logger.exception(f"Exception during analysis: {exception}",
|
|
213
|
-
exc_info=True)
|
|
214
208
|
from ...util.response_utils import build_error_job_response
|
|
215
209
|
job_response = build_error_job_response(
|
|
216
210
|
exception, job_response,
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetching.py
RENAMED
|
@@ -90,10 +90,6 @@ class JobFetching(ABC):
|
|
|
90
90
|
job_response.status_code = StatusCode.POLLING
|
|
91
91
|
|
|
92
92
|
except Exception as exception:
|
|
93
|
-
self.logger.bind(event='job_failed').exception(
|
|
94
|
-
"Exception when fetch job with provider_job_id {0}: {1}".format(
|
|
95
|
-
self.provider_job_id, str(exception)))
|
|
96
|
-
|
|
97
93
|
job_response = build_error_job_response(
|
|
98
94
|
exception, job_response,
|
|
99
95
|
message='Exception when fetch job with provider_job_id {0}'.format(
|
|
@@ -156,10 +152,6 @@ class JobFetching(ABC):
|
|
|
156
152
|
return job_response
|
|
157
153
|
|
|
158
154
|
except Exception as exception:
|
|
159
|
-
self.logger.exception(
|
|
160
|
-
"Exception when analyst job result with provider_job_id {0}: {1}".format(
|
|
161
|
-
self.provider_job_id, str(exception)))
|
|
162
|
-
|
|
163
155
|
job_response = build_error_job_response(
|
|
164
156
|
exception, job_response,
|
|
165
157
|
message='Exception when analyst job result with provider_job_id {0}'.format(
|
|
@@ -25,6 +25,17 @@ Ba vấn đề được xử lý ở đây:
|
|
|
25
25
|
Biến môi trường:
|
|
26
26
|
QUAPP_LOG_LEVEL DEBUG (default) | INFO | WARNING | ERROR
|
|
27
27
|
QUAPP_LOG_FORMAT text (default) | json | both
|
|
28
|
+
|
|
29
|
+
Chọn format nào:
|
|
30
|
+
|
|
31
|
+
- ``json`` -- dùng cho môi trường có Grafana/Loki. Mỗi record là MỘT dòng, luôn
|
|
32
|
+
có field ``jobId``, và traceback nằm trong ``exc.stackTrace`` của cùng dòng đó
|
|
33
|
+
nên filter theo ``jobId`` không làm mất stack.
|
|
34
|
+
- ``text`` -- dễ đọc bằng mắt qua ``docker logs``, nhưng traceback là các dòng
|
|
35
|
+
RIÊNG không có prefix ``[ConsoleJobLog][<job_id>]``. Filter theo job_id trên
|
|
36
|
+
Loki sẽ chỉ thấy dòng message và mất toàn bộ traceback.
|
|
37
|
+
- ``both`` -- CHỈ dùng khi debug. Mỗi record được ghi hai lần (một text, một
|
|
38
|
+
JSON), nên hệ thống thu log sẽ thấy mọi dòng nhân đôi.
|
|
28
39
|
"""
|
|
29
40
|
|
|
30
41
|
import inspect
|
|
@@ -149,15 +160,22 @@ def __custom_format(record) -> str:
|
|
|
149
160
|
level_color = level_colors.get(record['level'].name, 'white')
|
|
150
161
|
phase = f"[{extra['phase']}]" if extra.get('phase') else ''
|
|
151
162
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
163
|
+
# Traceback phải được append tường minh: khi format là một callable, loguru
|
|
164
|
+
# KHÔNG tự thêm. Format cũ thiếu hẳn phần này nên mọi logger.exception()
|
|
165
|
+
# trong codebase không in ra stack trace nào.
|
|
166
|
+
#
|
|
167
|
+
# Dùng extra[_exc_text] (do _prepare_record format sẵn VÀ đã redact) thay vì
|
|
168
|
+
# placeholder `{exception}` của loguru: `{exception}` được loguru render trực
|
|
169
|
+
# tiếp từ exception object, không đi qua redact(), nên secret nằm trong
|
|
170
|
+
# message của exception -- ví dụ response body của provider -- sẽ lọt nguyên
|
|
171
|
+
# vào log.
|
|
172
|
+
tail = '{extra[' + EXC_TEXT + ']}' if EXC_TEXT in extra else ''
|
|
173
|
+
|
|
156
174
|
return (f"<yellow>[ConsoleJobLog][{context}]{phase}</yellow> "
|
|
157
175
|
f"<{level_color}>{{level}}</{level_color}> : "
|
|
158
176
|
f"<green>{{time:YYYY-MM-DD HH:mm:ss.SSS}}</green> : "
|
|
159
177
|
f"<{level_color}>{{message}}</{level_color}>: "
|
|
160
|
-
f"<magenta>{{process}}</magenta>\n{
|
|
178
|
+
f"<magenta>{{process}}</magenta>\n{tail}")
|
|
161
179
|
|
|
162
180
|
|
|
163
181
|
# --------------------------------------------------------------------- json sink
|
|
@@ -273,8 +291,14 @@ def configure_logging(force: bool = False) -> None:
|
|
|
273
291
|
logger.remove()
|
|
274
292
|
|
|
275
293
|
if LOG_FORMAT in ('text', 'both'):
|
|
294
|
+
# diagnose=False là BẮT BUỘC: loguru diagnose in giá trị biến cục bộ
|
|
295
|
+
# trong traceback dưới dạng `└ 'value'`. Không có tên key nên hàm
|
|
296
|
+
# redact() -- vốn match `key: value` -- không mask được, và token của
|
|
297
|
+
# provider nằm trong biến cục bộ sẽ lọt nguyên vào log.
|
|
298
|
+
# backtrace=True vẫn giữ: nó mở rộng call chain nhưng không in giá trị.
|
|
276
299
|
logger.add(sink=sys.stderr, format=__custom_format, level=LOG_LEVEL,
|
|
277
|
-
colorize=True, filter=_prepare_record, enqueue=True
|
|
300
|
+
colorize=True, filter=_prepare_record, enqueue=True,
|
|
301
|
+
backtrace=True, diagnose=False)
|
|
278
302
|
|
|
279
303
|
if LOG_FORMAT in ('json', 'both'):
|
|
280
304
|
logger.add(sink=_json_sink, level=LOG_LEVEL, filter=_prepare_record,
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/custom_device.py
RENAMED
|
@@ -56,10 +56,6 @@ class CustomDevice(Device, ABC):
|
|
|
56
56
|
return job_response
|
|
57
57
|
|
|
58
58
|
except Exception as exception:
|
|
59
|
-
self.logger.exception(
|
|
60
|
-
f"Exception when analyzing job result: {exception}",
|
|
61
|
-
exc_info=True)
|
|
62
|
-
|
|
63
59
|
job_response = build_error_job_response(
|
|
64
60
|
exception, job_response,
|
|
65
61
|
message='Error when analyzing job result',
|
|
@@ -149,9 +149,6 @@ class Device(ABC):
|
|
|
149
149
|
# của build_error_job_response -- mất traceback, mất phân loại, và
|
|
150
150
|
# log ở mức user thường không bật. Đây lại là đường lỗi quan trọng
|
|
151
151
|
# nhất (submit circuit lên device), nên phải dùng đường chung.
|
|
152
|
-
self.logger.bind(event='job_failed').exception(
|
|
153
|
-
f"Execute job failed: {exception}")
|
|
154
|
-
|
|
155
152
|
job_response = build_error_job_response(
|
|
156
153
|
exception, job_response,
|
|
157
154
|
message='Error when executing job on device',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quapp-common
|
|
3
|
-
Version: 0.0.14.
|
|
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,6 +114,56 @@ Notes:
|
|
|
114
114
|
|
|
115
115
|
## Recently Changes Highlights
|
|
116
116
|
|
|
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
|
+
|
|
117
167
|
### v0.0.14.dev1 — Error taxonomy & error envelope (Phase 1)
|
|
118
168
|
|
|
119
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
|
|
@@ -92,10 +92,12 @@ quapp_common/util/response_utils.py
|
|
|
92
92
|
tests/test_circuit_adapter.py
|
|
93
93
|
tests/test_dispatcher_runner_error.py
|
|
94
94
|
tests/test_error_classifier.py
|
|
95
|
+
tests/test_error_log_dedupe.py
|
|
95
96
|
tests/test_error_report.py
|
|
96
97
|
tests/test_invocation_failure.py
|
|
97
98
|
tests/test_job_heartbeat.py
|
|
98
99
|
tests/test_json_parser_utils.py
|
|
99
100
|
tests/test_language_dispatcher.py
|
|
101
|
+
tests/test_log_secret_leak.py
|
|
100
102
|
tests/test_logging_config.py
|
|
101
103
|
tests/test_result_serializer.py
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# test_error_log_dedupe.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""Mỗi lần lỗi chỉ được phát ĐÚNG MỘT dòng ERROR event=job_failed.
|
|
6
|
+
|
|
7
|
+
Trước đây call-site log một lần rồi build_error_job_response() log lần nữa, nên
|
|
8
|
+
mỗi lỗi ra 2 dòng ERROR cùng event, mỗi dòng kèm một traceback. Ngoài chuyện ồn,
|
|
9
|
+
nó làm sai metric nếu backend đếm event=job_failed.
|
|
10
|
+
|
|
11
|
+
Test này cũng khoá luôn hành vi khi nhiều job chạy đồng thời: contextvars phải
|
|
12
|
+
cách ly, không job nào nhận log của job khác.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import re
|
|
17
|
+
from collections import Counter
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
from quapp_common.config import log_context
|
|
24
|
+
from quapp_common.config.log_context import bind_job, phase
|
|
25
|
+
from quapp_common.config.logging_config import _prepare_record, configure_logging
|
|
26
|
+
from quapp_common.config.thread_config import circuit_running_pool
|
|
27
|
+
from quapp_common.enum.runtime_phase import RuntimePhase
|
|
28
|
+
from quapp_common.util.response_utils import build_error_job_response
|
|
29
|
+
|
|
30
|
+
SOURCE_ROOT = Path(__file__).resolve().parent.parent / 'quapp_common'
|
|
31
|
+
|
|
32
|
+
# File có khối except gọi build_error_job_response().
|
|
33
|
+
CALL_SITE_FILES = (
|
|
34
|
+
'model/device/device.py',
|
|
35
|
+
'model/device/custom_device.py',
|
|
36
|
+
'component/backend/invocation.py',
|
|
37
|
+
'component/backend/job_fetcher.py',
|
|
38
|
+
'component/backend/job_fetching.py',
|
|
39
|
+
'async_tasks/post_processing_task.py',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(autouse=True)
|
|
44
|
+
def clean_context():
|
|
45
|
+
log_context.reset()
|
|
46
|
+
yield
|
|
47
|
+
log_context.reset()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def captured():
|
|
52
|
+
records = []
|
|
53
|
+
logger.remove()
|
|
54
|
+
sink_id = logger.add(
|
|
55
|
+
lambda message: records.append(
|
|
56
|
+
dict(message.record['extra'],
|
|
57
|
+
_level=message.record['level'].name,
|
|
58
|
+
_message=message.record['message'])),
|
|
59
|
+
level='DEBUG', filter=_prepare_record)
|
|
60
|
+
yield records
|
|
61
|
+
logger.remove(sink_id)
|
|
62
|
+
configure_logging(force=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def raise_and_catch(exception):
|
|
66
|
+
try:
|
|
67
|
+
raise exception
|
|
68
|
+
except BaseException as caught:
|
|
69
|
+
return caught
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------- mot loi = mot dong
|
|
73
|
+
|
|
74
|
+
def test_single_job_failed_line_per_error(captured):
|
|
75
|
+
bind_job('job_1')
|
|
76
|
+
|
|
77
|
+
build_error_job_response(raise_and_catch(ValueError('boom')),
|
|
78
|
+
message='Error when executing job on device',
|
|
79
|
+
phase=RuntimePhase.EXECUTION)
|
|
80
|
+
|
|
81
|
+
failed = [r for r in captured if r.get('event') == 'job_failed']
|
|
82
|
+
assert len(failed) == 1, f'mong doi 1 dong, nhan {len(failed)}'
|
|
83
|
+
assert failed[0]['_level'] == 'ERROR'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_job_failed_line_carries_classification(captured):
|
|
87
|
+
bind_job('job_1')
|
|
88
|
+
|
|
89
|
+
build_error_job_response(raise_and_catch(KeyError('shots')),
|
|
90
|
+
phase=RuntimePhase.PREPROCESSING)
|
|
91
|
+
|
|
92
|
+
failed = next(r for r in captured if r.get('event') == 'job_failed')
|
|
93
|
+
assert failed['category'] == 'INVALID_INPUT'
|
|
94
|
+
assert failed['error_code'] == 'INPUT_MISSING_FIELD'
|
|
95
|
+
assert failed['phase'] == 'PREPROCESSING'
|
|
96
|
+
assert failed['job_id'] == 'job_1'
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_phase_failed_is_separate_and_single(captured):
|
|
100
|
+
"""phase_failed là mốc timeline, khác job_failed -- không tính là trùng."""
|
|
101
|
+
bind_job('job_1')
|
|
102
|
+
|
|
103
|
+
with pytest.raises(ValueError):
|
|
104
|
+
with phase(RuntimePhase.COMPILATION):
|
|
105
|
+
raise ValueError('transpile failed')
|
|
106
|
+
|
|
107
|
+
events = Counter(r.get('event') for r in captured)
|
|
108
|
+
assert events['phase_failed'] == 1
|
|
109
|
+
assert events['job_failed'] == 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --------------------------------------------------- khong con log o call-site
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize('relative', CALL_SITE_FILES)
|
|
115
|
+
def test_no_redundant_logger_exception_before_builder(relative):
|
|
116
|
+
"""Khoá hồi quy ở mức source: không được log exception ngay trước
|
|
117
|
+
build_error_job_response()."""
|
|
118
|
+
source = (SOURCE_ROOT / relative).read_text(encoding='utf-8')
|
|
119
|
+
|
|
120
|
+
pattern = re.compile(
|
|
121
|
+
r'self\.logger(?:\.bind\([^)]*\))?\.exception\('
|
|
122
|
+
r'(?:[^()]|\([^()]*\))*\)\s*\n\s*\n?\s*'
|
|
123
|
+
r'(?:job_response\s*=\s*)?build_error_job_response',
|
|
124
|
+
re.MULTILINE)
|
|
125
|
+
|
|
126
|
+
assert not pattern.search(source), (
|
|
127
|
+
f'{relative}: con log exception ngay truoc '
|
|
128
|
+
f'build_error_job_response -- se ra 2 dong ERROR cho mot loi')
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------ dong thoi
|
|
132
|
+
|
|
133
|
+
def test_concurrent_jobs_do_not_duplicate_or_mix(captured):
|
|
134
|
+
"""4 job đồng thời: mỗi job đúng số dòng của nó, không lẫn context."""
|
|
135
|
+
|
|
136
|
+
def work(job_id):
|
|
137
|
+
bind_job(job_id, sdk='qiskit')
|
|
138
|
+
with phase(RuntimePhase.PREPROCESSING):
|
|
139
|
+
log_context.logger if False else None
|
|
140
|
+
from quapp_common.config.logging_config import job_logger
|
|
141
|
+
job_logger().info(f'step for {job_id}')
|
|
142
|
+
circuit_running_pool.submit(_analysis, job_id).result()
|
|
143
|
+
|
|
144
|
+
def _analysis(job_id):
|
|
145
|
+
from quapp_common.config.logging_config import job_logger
|
|
146
|
+
with phase(RuntimePhase.ANALYSIS):
|
|
147
|
+
job_logger().info(f'analysis for {job_id}')
|
|
148
|
+
|
|
149
|
+
async def run():
|
|
150
|
+
from starlette.concurrency import run_in_threadpool
|
|
151
|
+
await asyncio.gather(*[run_in_threadpool(work, f'job_{i}')
|
|
152
|
+
for i in range(4)])
|
|
153
|
+
|
|
154
|
+
asyncio.run(run())
|
|
155
|
+
|
|
156
|
+
per_job = Counter(r.get('job_id') for r in captured)
|
|
157
|
+
assert set(per_job) == {'job_0', 'job_1', 'job_2', 'job_3'}
|
|
158
|
+
assert len(set(per_job.values())) == 1, \
|
|
159
|
+
f'so dong khong deu giua cac job: {dict(per_job)}'
|
|
160
|
+
|
|
161
|
+
# Không dòng nào mang job_id của job khác so với message của nó.
|
|
162
|
+
mixed = [r for r in captured
|
|
163
|
+
if r.get('job_id') and 'job_' in r['_message']
|
|
164
|
+
and r['job_id'] not in r['_message']]
|
|
165
|
+
assert not mixed, f'{len(mixed)} dong lan context giua cac job'
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_thread_pool_keeps_phase(captured):
|
|
169
|
+
"""ThreadPoolExecutor thuần không copy contextvars; pool phải bọc lại."""
|
|
170
|
+
bind_job('job_pool')
|
|
171
|
+
|
|
172
|
+
def read_phase():
|
|
173
|
+
return log_context.current_phase_value()
|
|
174
|
+
|
|
175
|
+
with phase(RuntimePhase.ANALYSIS):
|
|
176
|
+
assert circuit_running_pool.submit(read_phase).result() == 'ANALYSIS'
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# test_log_secret_leak.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""Credential không được lọt vào log qua bất kỳ đường nào.
|
|
6
|
+
|
|
7
|
+
Có hai đường rò khác nhau và cần chặn bằng hai cơ chế khác nhau:
|
|
8
|
+
|
|
9
|
+
1. Secret nằm trong MESSAGE hoặc trong str(exception) -- dạng ``key: value``.
|
|
10
|
+
Chặn bằng ``redact()`` trong filter.
|
|
11
|
+
|
|
12
|
+
2. Secret nằm trong BIẾN CỤC BỘ của frame bị lỗi. loguru ``diagnose=True`` in
|
|
13
|
+
giá trị biến ra traceback dưới dạng ``└ 'value'`` -- không có tên key nên
|
|
14
|
+
``redact()`` không match. Chặn bằng ``diagnose=False`` ở sink.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import io
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
from quapp_common.config import log_context, logging_config
|
|
23
|
+
from quapp_common.config.log_context import bind_job
|
|
24
|
+
from quapp_common.config.logging_config import (REDACTED, _prepare_record,
|
|
25
|
+
configure_logging, job_logger)
|
|
26
|
+
|
|
27
|
+
TOKEN = 'glpat-SUPERSECRET-DO-NOT-LEAK'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture(autouse=True)
|
|
31
|
+
def clean_context():
|
|
32
|
+
log_context.reset()
|
|
33
|
+
yield
|
|
34
|
+
log_context.reset()
|
|
35
|
+
configure_logging(force=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def raw_sink(**add_kwargs) -> io.StringIO:
|
|
39
|
+
"""Sink THÔ với format mặc định của loguru.
|
|
40
|
+
|
|
41
|
+
Chỉ dùng để khoá hành vi của chính loguru. Lưu ý: sink kiểu này KHÔNG được
|
|
42
|
+
bảo vệ -- format mặc định render `{exception}` trực tiếp từ exception object,
|
|
43
|
+
bỏ qua redact(). Bảo vệ của ta nằm ở __custom_format + _prepare_record, nên
|
|
44
|
+
mọi sink phải được tạo qua configure_logging().
|
|
45
|
+
"""
|
|
46
|
+
buffer = io.StringIO()
|
|
47
|
+
logger.remove()
|
|
48
|
+
logger.add(buffer, level='DEBUG', filter=_prepare_record, colorize=False,
|
|
49
|
+
**add_kwargs)
|
|
50
|
+
return buffer
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def real_sink(monkeypatch) -> io.StringIO:
|
|
55
|
+
"""Sink text THẬT do configure_logging() tạo.
|
|
56
|
+
|
|
57
|
+
Sink bind vào sys.stderr lúc logger.add(), nên phải thay sys.stderr trước
|
|
58
|
+
khi cấu hình lại.
|
|
59
|
+
"""
|
|
60
|
+
buffer = io.StringIO()
|
|
61
|
+
monkeypatch.setattr('sys.stderr', buffer)
|
|
62
|
+
monkeypatch.setattr(logging_config, 'LOG_FORMAT', 'text')
|
|
63
|
+
configure_logging(force=True)
|
|
64
|
+
return buffer
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _connect(api_token):
|
|
68
|
+
"""api_token là biến cục bộ -- đúng thứ diagnose=True sẽ in ra."""
|
|
69
|
+
raise ConnectionError('provider refused the connection')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------- (1) secret trong message
|
|
73
|
+
|
|
74
|
+
def test_secret_in_message_is_redacted(real_sink):
|
|
75
|
+
bind_job('job_1')
|
|
76
|
+
|
|
77
|
+
job_logger().info(f"connecting with token: {TOKEN}")
|
|
78
|
+
|
|
79
|
+
logger.complete()
|
|
80
|
+
output = real_sink.getvalue()
|
|
81
|
+
assert TOKEN not in output
|
|
82
|
+
assert REDACTED in output
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_secret_in_exception_string_is_redacted(real_sink):
|
|
86
|
+
"""Secret nằm trong str(exception) -- ví dụ response body của provider bị
|
|
87
|
+
nhét vào ValueError. loguru render `{exception}` trực tiếp từ exception
|
|
88
|
+
object nên không qua redact(); vì vậy sink text phải dùng traceback đã
|
|
89
|
+
redact ở extra[_exc_text] thay cho placeholder đó."""
|
|
90
|
+
bind_job('job_1')
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
raise ValueError(f"auth failed for apiKey={TOKEN}")
|
|
94
|
+
except ValueError:
|
|
95
|
+
job_logger().exception('boom')
|
|
96
|
+
|
|
97
|
+
logger.complete()
|
|
98
|
+
output = real_sink.getvalue()
|
|
99
|
+
assert TOKEN not in output
|
|
100
|
+
assert REDACTED in output
|
|
101
|
+
assert 'Traceback' in output
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --------------------------------------- (2) secret trong bien cuc bo
|
|
105
|
+
|
|
106
|
+
def test_diagnose_true_would_leak_local_variables():
|
|
107
|
+
"""Khoá lại hành vi của loguru để test có ý nghĩa: với diagnose=True thì
|
|
108
|
+
token LỌT. Nếu loguru đổi hành vi này, test sẽ báo và ta biết có thể bật lại."""
|
|
109
|
+
buffer = raw_sink(backtrace=True, diagnose=True)
|
|
110
|
+
bind_job('job_1')
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
_connect(TOKEN)
|
|
114
|
+
except ConnectionError:
|
|
115
|
+
job_logger().exception('provider connection failed')
|
|
116
|
+
|
|
117
|
+
assert TOKEN in buffer.getvalue(), \
|
|
118
|
+
'loguru khong con in gia tri bien cuc bo -- xem lai co can diagnose=False'
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_diagnose_false_does_not_leak_local_variables():
|
|
122
|
+
buffer = raw_sink(backtrace=True, diagnose=False)
|
|
123
|
+
bind_job('job_1')
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
_connect(TOKEN)
|
|
127
|
+
except ConnectionError:
|
|
128
|
+
job_logger().exception('provider connection failed')
|
|
129
|
+
|
|
130
|
+
assert TOKEN not in buffer.getvalue()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------- json sink dung traceback sach
|
|
136
|
+
|
|
137
|
+
def test_json_stack_trace_has_no_variable_values():
|
|
138
|
+
"""stackTrace trong JSON lấy từ traceback.format_exception (chuẩn), không
|
|
139
|
+
kèm giá trị biến."""
|
|
140
|
+
buffer = raw_sink()
|
|
141
|
+
bind_job('job_1')
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
_connect(TOKEN)
|
|
145
|
+
except ConnectionError:
|
|
146
|
+
job_logger().exception('provider connection failed')
|
|
147
|
+
|
|
148
|
+
records = []
|
|
149
|
+
logger.remove()
|
|
150
|
+
logger.add(lambda message: records.append(message.record), level='DEBUG',
|
|
151
|
+
filter=_prepare_record)
|
|
152
|
+
bind_job('job_2')
|
|
153
|
+
try:
|
|
154
|
+
_connect(TOKEN)
|
|
155
|
+
except ConnectionError:
|
|
156
|
+
job_logger().exception('provider connection failed')
|
|
157
|
+
|
|
158
|
+
stack = records[-1]['extra'][logging_config.EXC_TEXT]
|
|
159
|
+
assert 'Traceback' in stack
|
|
160
|
+
assert TOKEN not in stack
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_task.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/circuit_adapter.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/device/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/result_serializer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/callback_url.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/authentication.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/custom_header.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/error_report.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/job_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/job_status.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/status_code.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/device_factory.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/handler_factory.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/provider_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/invocation_failure.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/json_parser_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|