sycommon-python-lib 0.2.8a13__py3-none-any.whl → 0.2.9__py3-none-any.whl

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.
@@ -178,6 +178,33 @@ class LogMasker(metaclass=SingletonMeta):
178
178
 
179
179
  return None
180
180
 
181
+ def sanitize_deep_str(self, value: Any, key: Any = None) -> Any:
182
+ """深 JSON 字符串脱敏:把整体序列化的大 JSON 逐层展开后按字段裁剪。
183
+
184
+ 背景:traceid 等中间件把 response_body / request_body 传成「整体 JSON
185
+ 字符串」——按标量路径只有 maxLength(1000) 一刀切,超限即头尾截断成
186
+ ``...[N chars truncated]...``,排障时无法 json.loads 还原,中段字段
187
+ (业务最关心的 data 部分)全部丢失。
188
+
189
+ 做法:字符串先尝试 json.loads 还原为结构;dict/list 逐层递归(每个
190
+ 叶子标量独立适用 maxLength),无法还原 / 解析失败则回退原字符串走
191
+ 标量截断。递归产物是结构化 dict/list,最终由调用方决定序列化方式——
192
+ 这样即使某个叶子被截断,其余字段仍完整可读。
193
+
194
+ 仅本地裁剪,不脱字段名(mask/drop 语义不变——命中字段名的值照常
195
+ 掩码/删除)。
196
+ """
197
+ if not isinstance(value, str):
198
+ return self.sanitize(value, key)
199
+ try:
200
+ parsed = json.loads(value)
201
+ except (json.JSONDecodeError, ValueError):
202
+ return self._truncate_str(value)
203
+ if not isinstance(parsed, (dict, list)):
204
+ # "123" / "true" 这类 JSON 标量字符串:还原后反而丢引号语义,按原串截断
205
+ return self._truncate_str(value)
206
+ return self._sanitize_value(parsed, key, depth=0)
207
+
181
208
  def _truncate_str(self, s: str) -> str:
182
209
  n = self._config.get("maxLength", 1000)
183
210
  if len(s) <= n:
@@ -4,6 +4,7 @@ import re
4
4
  from typing import Dict, Any
5
5
  from fastapi import Request, Response
6
6
  from sycommon.logging.kafka_log import SYLogger
7
+ from sycommon.logging.log_masker import masker as _log_masker
7
8
  from sycommon.tools.merge_headers import merge_headers
8
9
  from sycommon.tools.snowflake import Snowflake
9
10
 
@@ -316,15 +317,26 @@ def setup_trace_id_handler(app):
316
317
  response.headers[k] = v
317
318
 
318
319
  # 构建响应日志
319
- response_log_body = response_body.decode('utf-8', errors='ignore')
320
- # 检测是否包含 base64 二进制内容(如 PDF、图片等),如有则省略
321
- try:
322
- resp_json = json.loads(response_log_body)
323
- if isinstance(resp_json, dict) and resp_json.get("content") and len(str(resp_json["content"])) > 1000:
324
- resp_json["content"] = f"... (base64/binary content omitted, {len(str(resp_json['content']))} chars)"
325
- response_log_body = json.dumps(resp_json, ensure_ascii=False)
326
- except (json.JSONDecodeError, ValueError):
327
- pass
320
+ # response_body 是「整体 JSON 字符串」——按标量路径只受 maxLength(1000)
321
+ # 一刀切截断,中段字段丢失且无法解码还原。这里走深 JSON 脱敏:逐层展开后
322
+ # 每个叶子字段独立适用 maxLength,结构完整、json.loads 可还原,排障时
323
+ # 能直接读 data 内任意字段。解析失败(非 JSON)自动回退标量截断。
324
+ response_log_body = _log_masker.sanitize_deep_str(
325
+ response_body.decode('utf-8', errors='ignore'))
326
+ # 检测是否包含 base64 二进制内容(如 PDF、图片等),如有则省略。
327
+ # sanitize_deep_str 已把 JSON 字符串还原成结构化 dict——直接在结构上
328
+ # 改(不要再 json.loads 一个 dict,会 TypeError);仅字符串回退路径走解析。
329
+ if isinstance(response_log_body, dict):
330
+ if response_log_body.get("content") and len(str(response_log_body["content"])) > 1000:
331
+ response_log_body["content"] = f"... (base64/binary content omitted, {len(str(response_log_body['content']))} chars)"
332
+ else:
333
+ try:
334
+ resp_json = json.loads(response_log_body)
335
+ if isinstance(resp_json, dict) and resp_json.get("content") and len(str(resp_json["content"])) > 1000:
336
+ resp_json["content"] = f"... (base64/binary content omitted, {len(str(resp_json['content']))} chars)"
337
+ response_log_body = json.dumps(resp_json, ensure_ascii=False)
338
+ except (json.JSONDecodeError, ValueError):
339
+ pass
328
340
  response_message = {
329
341
  "traceId": trace_id,
330
342
  "status_code": response.status_code,
@@ -153,6 +153,27 @@ class RabbitMQClient:
153
153
  logger.debug(f"探测队列 '{self.queue_name}' 消费者数失败: {e}")
154
154
  return None
155
155
 
156
+ def _live_aiormq_consumer_tags(self):
157
+ """取 aiormq 层(broker 真实状态)活跃 consumer tag 集合。
158
+
159
+ 与 local_consumer_alive 同款内省路径。返回 None 表示无法内省
160
+ (调用方退化为不过滤,保持旧行为);空集/非空集均可用于过滤
161
+ aio-pika RobustQueue._consumers 里的腰斩残留死 tag。"""
162
+ channel = self._channel
163
+ if channel is None:
164
+ return None
165
+ try:
166
+ underlay = channel._channel
167
+ if underlay is None:
168
+ return None
169
+ target = getattr(underlay, "channel", None) or underlay
170
+ consumers = getattr(target, "consumers", None)
171
+ if isinstance(consumers, dict):
172
+ return set(consumers.keys())
173
+ return None
174
+ except Exception:
175
+ return None
176
+
156
177
  def local_consumer_alive(self) -> bool:
157
178
  """本实例视角的消费者活性(本地注册表快速判定)。
158
179
 
@@ -600,18 +621,39 @@ class RabbitMQClient:
600
621
  # 则仍按显式 tag 真正 consume,交给服务端校验。
601
622
  existing = getattr(self._queue, "_consumers", None)
602
623
  if isinstance(existing, dict):
603
- if existing and consumer_tag is None:
604
- self._consumer_tag = next(iter(existing))
624
+ # 🔒 tag 不复用(2026-08-26 实证修复):aio-pika 的
625
+ # RobustQueue.cancel cancel 腰斩(如监控看门狗超时传播)时,
626
+ # super().cancel() 未返回 → _consumers 永不 pop → 死 tag 残留。
627
+ # 复用层只看 aio-pika 注册表会"假恢复"(本地以为在消费,broker
628
+ # 侧没有)。复用前以 aiormq 注册表(broker 真实状态)过滤:
629
+ # 只复用两边都在的 tag;被过滤掉视为残留,走真 consume 重建。
630
+ aiormq_tags = self._live_aiormq_consumer_tags()
631
+ live_existing = {t: v for t, v in existing.items()
632
+ if t in aiormq_tags} if aiormq_tags is not None else existing
633
+ if live_existing and consumer_tag is None:
634
+ self._consumer_tag = next(iter(live_existing))
605
635
  logger.warning(
606
- f"队列 '{getattr(self._queue, 'name', '?')}' 已有 {len(existing)} 个消费者,"
636
+ f"队列 '{getattr(self._queue, 'name', '?')}' 已有 {len(live_existing)} 个消费者,"
607
637
  f"跳过重复注册(复用现有 tag: {self._consumer_tag})")
608
638
  return self._consumer_tag
609
- if consumer_tag is not None and consumer_tag in existing:
639
+ if consumer_tag is not None and consumer_tag in live_existing:
610
640
  self._consumer_tag = consumer_tag
611
641
  logger.info(
612
642
  f"队列 '{getattr(self._queue, 'name', '?')}' 的 tag {consumer_tag} "
613
643
  f"已在消费,跳过重复注册(幂等)")
614
644
  return self._consumer_tag
645
+ if existing and not live_existing:
646
+ logger.warning(
647
+ f"队列 '{getattr(self._queue, 'name', '?')}' 的 {len(existing)} 个"
648
+ f"注册 tag 均不在 aiormq 活跃注册表(疑似腰斩残留),"
649
+ f"不复用,强制重新注册消费")
650
+ # 🔒 显式传入的 tag 若已被判死(不在 aiormq 活跃集),
651
+ # 不能再传给 consume:aiormq 本地注册表虽无此 tag,但若
652
+ # broker 侧仍存活(本地注册表 lag 的误判场景),重复
653
+ # basic_consume 同 tag 会触发 DuplicateConsumerTag 异常。
654
+ # 置 None 让 broker 分配全新 tag。
655
+ if consumer_tag is not None:
656
+ consumer_tag = None
615
657
 
616
658
  self._consumer_tag = await self._queue.consume(
617
659
  self._process_message_callback, consumer_tag=consumer_tag)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.8a13
3
+ Version: 0.2.9
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -221,7 +221,7 @@ sycommon/llm/tiktoken_cache/fb374d419588a4632f3f557e76b4b70aebbca790,sha256=RGqV
221
221
  sycommon/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
222
222
  sycommon/logging/async_sql_logger.py,sha256=TIUAqK8_F9H7Graw00hNjEI3DJ1ZRo5fRUcnqaMZlbU,2187
223
223
  sycommon/logging/kafka_log.py,sha256=lAnwb0yhGM44L9xRrY7z-mAt-rOUew-6gmiYbZH9_3Y,44661
224
- sycommon/logging/log_masker.py,sha256=GYf4NA1YXF070r0MUkj2oh4bygaZo0hEnDih734a-eE,15923
224
+ sycommon/logging/log_masker.py,sha256=WjHXKOmgnEItJzEntFXZM6o0Gc1gATp_I8bxkegHs8w,17449
225
225
  sycommon/logging/logger_levels.py,sha256=xJBzPV2H7GpEQggOIjyckEsA4yf6s4NpI9ckgO3xWKA,1154
226
226
  sycommon/logging/logger_wrapper.py,sha256=TtzmGw_K5NjBGBq9Gjqtnh834izAv8TQwsOeL2fIbAI,481
227
227
  sycommon/logging/process_logger.py,sha256=Lnp7ptU9Uyw18OFBfNxlT9tHPiiWIv9jB8O6IhbELAQ,6266
@@ -242,7 +242,7 @@ sycommon/middleware/sandbox_gateway.py,sha256=ZyXV9iwvaLNNTk0oM-RdjGRSgkBPaPp67A
242
242
  sycommon/middleware/timeout.py,sha256=KlxOPa8xl2dg6yuRi_EzkVJG8bX4stb5ueYxctzzGM8,1433
243
243
  sycommon/middleware/token_tracking.py,sha256=rEbgV1bgWMdzAERx4aq5XAvOIT6jTY_tK1P0xHJnL3o,6609
244
244
  sycommon/middleware/tool_result_truncation.py,sha256=xtwBf8q11Z8EPzmo3Ju4OfUbULOklSsWo_oystxCW0o,13640
245
- sycommon/middleware/traceid.py,sha256=JoHDW5hSiBi0tjjfkidjpV5L_iGVVLEpnnSnVQs1spo,18329
245
+ sycommon/middleware/traceid.py,sha256=cNXDzmVwE--QrpiJRAWg-_eoXuXExqlAZpEWWeV6hQA,19401
246
246
  sycommon/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
247
247
  sycommon/models/base_http.py,sha256=efxtsxmQmInlsRVqQU9P17MSVXx3TsNmgimMlz0hNV0,4788
248
248
  sycommon/models/log.py,sha256=rZpj6VkDRxK3B6H7XSeWdYZshU8F0Sks8bq1p6pPlDw,500
@@ -260,7 +260,7 @@ sycommon/notice/uvicorn_monitor.py,sha256=_SlLB91N7g-Jt_xP_AX1IWFgA7_1lCqsQFwm_Y
260
260
  sycommon/notice/wecom_message.py,sha256=_oA6hg0Ask7NkFbvUpL78WOmyUb6uW7KcY71GhNKiJY,12363
261
261
  sycommon/rabbitmq/process_pool_consumer.py,sha256=EXdNypgathjTfPqkccoAW0HiVB9HuZLGhPbWUcYh0_Q,29750
262
262
  sycommon/rabbitmq/rabbit_management_client.py,sha256=cf7GSbvdzOA6vFV4jC3IL6wh8yu8IEDgTPHUqRV2_uE,4021
263
- sycommon/rabbitmq/rabbitmq_client.py,sha256=G36mcyFdrMSHO5FeMcDaVPEN_d8R2u18GCRHG_v8eE8,37343
263
+ sycommon/rabbitmq/rabbitmq_client.py,sha256=_eamRhy_Tqcbj0GwTFm8ALoG9pa71a6yChrthLizh44,39806
264
264
  sycommon/rabbitmq/rabbitmq_pool.py,sha256=alZ4vDZAgulSaDbkGJy04nxh239wAVxiYvm2DTLXSBE,18163
265
265
  sycommon/rabbitmq/rabbitmq_service.py,sha256=EBepnotvzZvj34t40HEDup_GAtJI_i1xb795n3nh244,10043
266
266
  sycommon/rabbitmq/rabbitmq_service_client_manager.py,sha256=UZgpD6n0mdo5u3gaGW-jhUuPV50bRNdXFESzL9ABsR4,10623
@@ -334,8 +334,8 @@ sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
334
334
  sycommon/tools/user_id.py,sha256=o-zsubGSR5rSSZf8Pk8B7AhE21ahDTTIW5JZmJO6Flw,4761
335
335
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
336
336
  sycommon/xxljob/xxljob_service.py,sha256=1yifwIBNGsCIxLnQjHKiBlbsigc_zvPH-dMTZcNxe-Q,7649
337
- sycommon_python_lib-0.2.8a13.dist-info/METADATA,sha256=5hqjCXuLwT_Xod6rUQqUIuZF1tjiMJqh2akzY0LDCWM,8033
338
- sycommon_python_lib-0.2.8a13.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
339
- sycommon_python_lib-0.2.8a13.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
340
- sycommon_python_lib-0.2.8a13.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
341
- sycommon_python_lib-0.2.8a13.dist-info/RECORD,,
337
+ sycommon_python_lib-0.2.9.dist-info/METADATA,sha256=1yHtlqyJ9ed325kCGRfwls1AggAO0hQhB0-y8LKq9Ak,8030
338
+ sycommon_python_lib-0.2.9.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
339
+ sycommon_python_lib-0.2.9.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
340
+ sycommon_python_lib-0.2.9.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
341
+ sycommon_python_lib-0.2.9.dist-info/RECORD,,