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.
Files changed (105) hide show
  1. {quapp_common-0.0.14.dev1/quapp_common.egg-info → quapp_common-0.0.14.dev2}/PKG-INFO +51 -1
  2. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/README.md +50 -0
  3. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/pyproject.toml +1 -1
  4. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/post_processing_task.py +0 -3
  5. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/invocation.py +0 -11
  6. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetcher.py +0 -6
  7. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_fetching.py +0 -8
  8. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/logging_config.py +30 -6
  9. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/custom_device.py +0 -4
  10. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/device.py +0 -3
  11. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2/quapp_common.egg-info}/PKG-INFO +51 -1
  12. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/SOURCES.txt +2 -0
  13. quapp_common-0.0.14.dev2/tests/test_error_log_dedupe.py +176 -0
  14. quapp_common-0.0.14.dev2/tests/test_log_secret_leak.py +160 -0
  15. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/LICENSE +0 -0
  16. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/__init__.py +0 -0
  17. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/__init__.py +0 -0
  18. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_invocation_task.py +0 -0
  19. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/async_task.py +0 -0
  20. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  21. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/__init__.py +0 -0
  22. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/__init__.py +0 -0
  23. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_heartbeat.py +0 -0
  24. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/backend/job_manager.py +0 -0
  25. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/bridge.py +0 -0
  26. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/__init__.py +0 -0
  27. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/callback/update_job_metadata.py +0 -0
  28. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/circuit_adapter.py +0 -0
  29. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/device/__init__.py +0 -0
  30. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/device/device_selection.py +0 -0
  31. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/dispatcher.py +0 -0
  32. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/component/result_serializer.py +0 -0
  33. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/__init__.py +0 -0
  34. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/log_context.py +0 -0
  35. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/config/thread_config.py +0 -0
  36. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/__init__.py +0 -0
  37. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/__init__.py +0 -0
  38. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  39. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  40. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  41. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/backend/__init__.py +0 -0
  42. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/backend/backend_information.py +0 -0
  43. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/__init__.py +0 -0
  44. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/callback/callback_url.py +0 -0
  45. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/device/__init__.py +0 -0
  46. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/device/circuit_running_option.py +0 -0
  47. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/__init__.py +0 -0
  48. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/post_processing_promise.py +0 -0
  49. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/promise/promise.py +0 -0
  50. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/__init__.py +0 -0
  51. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/invocation_request.py +0 -0
  52. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/job_fetching_request.py +0 -0
  53. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/request/request.py +0 -0
  54. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/__init__.py +0 -0
  55. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/authentication.py +0 -0
  56. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/custom_header.py +0 -0
  57. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/error_report.py +0 -0
  58. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/data/response/job_response.py +0 -0
  59. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/__init__.py +0 -0
  60. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/base_enum.py +0 -0
  61. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/error_category.py +0 -0
  62. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/http_header.py +0 -0
  63. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/invocation_step.py +0 -0
  64. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/language.py +0 -0
  65. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/media_type.py +0 -0
  66. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/processing_unit.py +0 -0
  67. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/provider_tag.py +0 -0
  68. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/runtime_phase.py +0 -0
  69. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/sdk.py +0 -0
  70. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/__init__.py +0 -0
  71. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/job_status.py +0 -0
  72. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/status/status_code.py +0 -0
  73. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/enum/token_type.py +0 -0
  74. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/__init__.py +0 -0
  75. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/device_factory.py +0 -0
  76. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/handler_factory.py +0 -0
  77. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/factory/provider_factory.py +0 -0
  78. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/handler/__init__.py +0 -0
  79. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/handler/handler.py +0 -0
  80. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/__init__.py +0 -0
  81. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/device/__init__.py +0 -0
  82. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/invocation.py +0 -0
  83. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/__init__.py +0 -0
  84. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/model/provider/provider.py +0 -0
  85. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/__init__.py +0 -0
  86. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/error_classifier.py +0 -0
  87. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/file_utils.py +0 -0
  88. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/http_utils.py +0 -0
  89. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/invocation_failure.py +0 -0
  90. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/json_parser_utils.py +0 -0
  91. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common/util/response_utils.py +0 -0
  92. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/dependency_links.txt +0 -0
  93. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/requires.txt +0 -0
  94. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/quapp_common.egg-info/top_level.txt +0 -0
  95. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/setup.cfg +0 -0
  96. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_circuit_adapter.py +0 -0
  97. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_dispatcher_runner_error.py +0 -0
  98. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_error_classifier.py +0 -0
  99. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_error_report.py +0 -0
  100. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_invocation_failure.py +0 -0
  101. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_job_heartbeat.py +0 -0
  102. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_json_parser_utils.py +0 -0
  103. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_language_dispatcher.py +0 -0
  104. {quapp_common-0.0.14.dev1 → quapp_common-0.0.14.dev2}/tests/test_logging_config.py +0 -0
  105. {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.dev1
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.dev1"
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',
@@ -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',
@@ -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,
@@ -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
- # `{exception}` bắt buộc: khi format là một callable, loguru KHÔNG tự
153
- # append traceback. Format cũ thiếu placeholder này, nên mọi
154
- # logger.exception(...) trong codebase đều không in ra stack trace nào --
155
- # đúng phần thông tin cần nhất khi debug.
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{{exception}}")
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,
@@ -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.dev1
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