tamar-file-hub-client 0.1.4__py3-none-any.whl → 0.1.5__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.
- file_hub_client/rpc/gen/file_service_pb2.py +30 -30
- file_hub_client/rpc/interceptors.py +578 -580
- file_hub_client/rpc/protos/file_service.proto +2 -1
- file_hub_client/schemas/file.py +171 -170
- file_hub_client/services/file/async_blob_service.py +1 -0
- file_hub_client/services/file/sync_blob_service.py +1 -0
- file_hub_client/utils/logging.py +335 -318
- {tamar_file_hub_client-0.1.4.dist-info → tamar_file_hub_client-0.1.5.dist-info}/METADATA +17 -2
- {tamar_file_hub_client-0.1.4.dist-info → tamar_file_hub_client-0.1.5.dist-info}/RECORD +11 -11
- {tamar_file_hub_client-0.1.4.dist-info → tamar_file_hub_client-0.1.5.dist-info}/WHEEL +0 -0
- {tamar_file_hub_client-0.1.4.dist-info → tamar_file_hub_client-0.1.5.dist-info}/top_level.txt +0 -0
@@ -1,581 +1,579 @@
|
|
1
|
-
"""
|
2
|
-
gRPC拦截器,用于自动记录请求和响应日志
|
3
|
-
"""
|
4
|
-
import time
|
5
|
-
import asyncio
|
6
|
-
from typing import Any, Callable, Optional, Dict, List, Tuple, Union
|
7
|
-
import grpc
|
8
|
-
from grpc import aio
|
9
|
-
import base64
|
10
|
-
import re
|
11
|
-
|
12
|
-
from ..utils.logging import get_logger, GrpcRequestLogger
|
13
|
-
|
14
|
-
|
15
|
-
def _extract_method_name(method: str) -> str:
|
16
|
-
"""从gRPC方法路径中提取方法名"""
|
17
|
-
# 方法格式: /package.service/MethodName
|
18
|
-
parts = method.split('/')
|
19
|
-
if len(parts) >= 2:
|
20
|
-
return parts[-1]
|
21
|
-
return method
|
22
|
-
|
23
|
-
def _extract_request_id(metadata: List[Tuple[str, str]]) -> str:
|
24
|
-
"""从元数据中提取请求ID"""
|
25
|
-
if not metadata:
|
26
|
-
return "unknown"
|
27
|
-
|
28
|
-
try:
|
29
|
-
metadata_dict = dict(metadata)
|
30
|
-
return metadata_dict.get('x-request-id', 'unknown')
|
31
|
-
except Exception:
|
32
|
-
# 如果元数据解析失败,返回unknown
|
33
|
-
return "unknown"
|
34
|
-
|
35
|
-
def _metadata_to_dict(metadata: List[Tuple[str, str]]) -> Dict[str, str]:
|
36
|
-
"""将元数据转换为字典"""
|
37
|
-
try:
|
38
|
-
return dict(metadata) if metadata else {}
|
39
|
-
except Exception:
|
40
|
-
# 如果元数据解析失败,返回空字典
|
41
|
-
return {}
|
42
|
-
|
43
|
-
|
44
|
-
def _is_base64_string(value: str, min_length: int = 100) -> bool:
|
45
|
-
"""检查字符串是否可能是base64编码的内容"""
|
46
|
-
if not isinstance(value, str) or len(value) < min_length:
|
47
|
-
return False
|
48
|
-
|
49
|
-
# 基本的base64格式检查
|
50
|
-
base64_pattern = re.compile(r'^[A-Za-z0-9+/]*={0,2}$')
|
51
|
-
if not base64_pattern.match(value):
|
52
|
-
return False
|
53
|
-
|
54
|
-
# 尝试解码以验证
|
55
|
-
try:
|
56
|
-
decoded = base64.b64decode(value, validate=True)
|
57
|
-
# 如果能成功解码且长度超过阈值,认为是base64
|
58
|
-
return len(decoded) > 50
|
59
|
-
except:
|
60
|
-
return False
|
61
|
-
|
62
|
-
|
63
|
-
def _truncate_long_string(value: str, max_length: int = 200, placeholder: str = "...") -> str:
|
64
|
-
"""截断长字符串,保留开头和结尾部分"""
|
65
|
-
if len(value) <= max_length:
|
66
|
-
return value
|
67
|
-
|
68
|
-
# 计算保留的开头和结尾长度
|
69
|
-
keep_length = (max_length - len(placeholder)) // 2
|
70
|
-
return f"{value[:keep_length]}{placeholder}{value[-keep_length:]}"
|
71
|
-
|
72
|
-
|
73
|
-
def _sanitize_request_data(data: Any, max_string_length: int = 200, max_binary_preview: int = 50) -> Any:
|
74
|
-
"""
|
75
|
-
清理请求数据,截断长字符串和二进制内容
|
76
|
-
|
77
|
-
Args:
|
78
|
-
data: 要清理的数据
|
79
|
-
max_string_length: 字符串的最大长度
|
80
|
-
max_binary_preview: 二进制内容预览的最大长度
|
81
|
-
|
82
|
-
Returns:
|
83
|
-
清理后的数据
|
84
|
-
"""
|
85
|
-
if isinstance(data, dict):
|
86
|
-
# 递归处理字典
|
87
|
-
result = {}
|
88
|
-
for key, value in data.items():
|
89
|
-
# 检查是否是需要特殊处理的字段
|
90
|
-
if key.lower() in ['operations'] and isinstance(value, list):
|
91
|
-
# 对于 operations 字段,特殊处理以显示操作类型和数量
|
92
|
-
if len(value) > 5:
|
93
|
-
ops_summary = []
|
94
|
-
# 统计操作类型
|
95
|
-
op_types = {}
|
96
|
-
for op in value:
|
97
|
-
if isinstance(op, dict):
|
98
|
-
for op_type in ['edit', 'create', 'update', 'delete', 'clear']:
|
99
|
-
if op_type in op:
|
100
|
-
op_types[op_type] = op_types.get(op_type, 0) + 1
|
101
|
-
|
102
|
-
# 显示前3个操作
|
103
|
-
for i in range(min(3, len(value))):
|
104
|
-
ops_summary.append(_sanitize_request_data(value[i], max_string_length, max_binary_preview))
|
105
|
-
|
106
|
-
# 添加统计信息
|
107
|
-
ops_summary.append(f"... 总计 {len(value)} 个操作: {', '.join(f'{k}={v}' for k, v in op_types.items())}")
|
108
|
-
result[key] = ops_summary
|
109
|
-
else:
|
110
|
-
result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
|
111
|
-
elif key.lower() in ['content', 'data', 'file', 'file_content', 'binary', 'blob', 'bytes', 'image', 'attachment']:
|
112
|
-
if isinstance(value, (bytes, bytearray)):
|
113
|
-
# 二进制内容,显示长度和预览
|
114
|
-
preview = base64.b64encode(value[:max_binary_preview]).decode('utf-8')
|
115
|
-
result[key] = f"<binary {len(value)} bytes, preview: {preview}...>"
|
116
|
-
elif isinstance(value, str):
|
117
|
-
# 检查是否是base64字符串
|
118
|
-
if _is_base64_string(value):
|
119
|
-
result[key] = f"<base64 string, length: {len(value)}, preview: {value[:max_binary_preview]}...>"
|
120
|
-
elif len(value) > max_string_length:
|
121
|
-
result[key] = _truncate_long_string(value, max_string_length)
|
122
|
-
else:
|
123
|
-
result[key] = value
|
124
|
-
else:
|
125
|
-
result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
|
126
|
-
else:
|
127
|
-
result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
|
128
|
-
return result
|
129
|
-
elif isinstance(data, list):
|
130
|
-
# 递归处理列表,限制列表长度以避免日志过长
|
131
|
-
max_list_items = 10 # 最多显示10个元素
|
132
|
-
if len(data) > max_list_items:
|
133
|
-
# 显示前5个和后5个元素
|
134
|
-
preview_items = (
|
135
|
-
[_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data[:5]] +
|
136
|
-
[f"... {len(data) - max_list_items} more items ..."] +
|
137
|
-
[_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data[-5:]]
|
138
|
-
)
|
139
|
-
return preview_items
|
140
|
-
else:
|
141
|
-
return [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data]
|
142
|
-
elif isinstance(data, tuple):
|
143
|
-
# 递归处理元组
|
144
|
-
return tuple(_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data)
|
145
|
-
elif isinstance(data, (bytes, bytearray)):
|
146
|
-
# 二进制内容
|
147
|
-
preview = base64.b64encode(data[:max_binary_preview]).decode('utf-8')
|
148
|
-
return f"<binary {len(data)} bytes, preview: {preview}...>"
|
149
|
-
elif isinstance(data, str):
|
150
|
-
# 字符串内容
|
151
|
-
if _is_base64_string(data):
|
152
|
-
return f"<base64 string, length: {len(data)}, preview: {data[:max_binary_preview]}...>"
|
153
|
-
elif len(data) > max_string_length:
|
154
|
-
return _truncate_long_string(data, max_string_length)
|
155
|
-
else:
|
156
|
-
return data
|
157
|
-
else:
|
158
|
-
# 其他类型直接返回
|
159
|
-
return data
|
160
|
-
|
161
|
-
|
162
|
-
class LoggingInterceptor:
|
163
|
-
"""gRPC日志拦截器基类"""
|
164
|
-
|
165
|
-
def __init__(self):
|
166
|
-
self.logger = get_logger()
|
167
|
-
self.request_logger = GrpcRequestLogger(self.logger)
|
168
|
-
|
169
|
-
|
170
|
-
class AsyncUnaryUnaryLoggingInterceptor(LoggingInterceptor, aio.UnaryUnaryClientInterceptor):
|
171
|
-
"""异步一元-一元gRPC日志拦截器"""
|
172
|
-
|
173
|
-
async def intercept_unary_unary(
|
174
|
-
self,
|
175
|
-
continuation: Callable,
|
176
|
-
client_call_details: grpc.aio.ClientCallDetails,
|
177
|
-
request: Any
|
178
|
-
) -> Any:
|
179
|
-
"""拦截一元-一元调用"""
|
180
|
-
# 安全提取方法名
|
181
|
-
try:
|
182
|
-
method = client_call_details.method
|
183
|
-
if isinstance(method, bytes):
|
184
|
-
method = method.decode('utf-8', errors='ignore')
|
185
|
-
method_name = method.split('/')[-1] if '/' in method else method
|
186
|
-
except:
|
187
|
-
method_name = "unknown"
|
188
|
-
|
189
|
-
# 安全提取request_id
|
190
|
-
request_id = "unknown"
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
request_data
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
method
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
request_data
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
method
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
method
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
method
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
try:
|
503
|
-
if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
request_data =
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
return [
|
580
|
-
SyncUnaryUnaryLoggingInterceptor(),
|
1
|
+
"""
|
2
|
+
gRPC拦截器,用于自动记录请求和响应日志
|
3
|
+
"""
|
4
|
+
import time
|
5
|
+
import asyncio
|
6
|
+
from typing import Any, Callable, Optional, Dict, List, Tuple, Union
|
7
|
+
import grpc
|
8
|
+
from grpc import aio
|
9
|
+
import base64
|
10
|
+
import re
|
11
|
+
|
12
|
+
from ..utils.logging import get_logger, GrpcRequestLogger
|
13
|
+
|
14
|
+
|
15
|
+
def _extract_method_name(method: str) -> str:
|
16
|
+
"""从gRPC方法路径中提取方法名"""
|
17
|
+
# 方法格式: /package.service/MethodName
|
18
|
+
parts = method.split('/')
|
19
|
+
if len(parts) >= 2:
|
20
|
+
return parts[-1]
|
21
|
+
return method
|
22
|
+
|
23
|
+
def _extract_request_id(metadata: List[Tuple[str, str]]) -> str:
|
24
|
+
"""从元数据中提取请求ID"""
|
25
|
+
if not metadata:
|
26
|
+
return "unknown"
|
27
|
+
|
28
|
+
try:
|
29
|
+
metadata_dict = dict(metadata)
|
30
|
+
return metadata_dict.get('x-request-id', 'unknown')
|
31
|
+
except Exception:
|
32
|
+
# 如果元数据解析失败,返回unknown
|
33
|
+
return "unknown"
|
34
|
+
|
35
|
+
def _metadata_to_dict(metadata: List[Tuple[str, str]]) -> Dict[str, str]:
|
36
|
+
"""将元数据转换为字典"""
|
37
|
+
try:
|
38
|
+
return dict(metadata) if metadata else {}
|
39
|
+
except Exception:
|
40
|
+
# 如果元数据解析失败,返回空字典
|
41
|
+
return {}
|
42
|
+
|
43
|
+
|
44
|
+
def _is_base64_string(value: str, min_length: int = 100) -> bool:
|
45
|
+
"""检查字符串是否可能是base64编码的内容"""
|
46
|
+
if not isinstance(value, str) or len(value) < min_length:
|
47
|
+
return False
|
48
|
+
|
49
|
+
# 基本的base64格式检查
|
50
|
+
base64_pattern = re.compile(r'^[A-Za-z0-9+/]*={0,2}$')
|
51
|
+
if not base64_pattern.match(value):
|
52
|
+
return False
|
53
|
+
|
54
|
+
# 尝试解码以验证
|
55
|
+
try:
|
56
|
+
decoded = base64.b64decode(value, validate=True)
|
57
|
+
# 如果能成功解码且长度超过阈值,认为是base64
|
58
|
+
return len(decoded) > 50
|
59
|
+
except:
|
60
|
+
return False
|
61
|
+
|
62
|
+
|
63
|
+
def _truncate_long_string(value: str, max_length: int = 200, placeholder: str = "...") -> str:
|
64
|
+
"""截断长字符串,保留开头和结尾部分"""
|
65
|
+
if len(value) <= max_length:
|
66
|
+
return value
|
67
|
+
|
68
|
+
# 计算保留的开头和结尾长度
|
69
|
+
keep_length = (max_length - len(placeholder)) // 2
|
70
|
+
return f"{value[:keep_length]}{placeholder}{value[-keep_length:]}"
|
71
|
+
|
72
|
+
|
73
|
+
def _sanitize_request_data(data: Any, max_string_length: int = 200, max_binary_preview: int = 50) -> Any:
|
74
|
+
"""
|
75
|
+
清理请求数据,截断长字符串和二进制内容
|
76
|
+
|
77
|
+
Args:
|
78
|
+
data: 要清理的数据
|
79
|
+
max_string_length: 字符串的最大长度
|
80
|
+
max_binary_preview: 二进制内容预览的最大长度
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
清理后的数据
|
84
|
+
"""
|
85
|
+
if isinstance(data, dict):
|
86
|
+
# 递归处理字典
|
87
|
+
result = {}
|
88
|
+
for key, value in data.items():
|
89
|
+
# 检查是否是需要特殊处理的字段
|
90
|
+
if key.lower() in ['operations'] and isinstance(value, list):
|
91
|
+
# 对于 operations 字段,特殊处理以显示操作类型和数量
|
92
|
+
if len(value) > 5:
|
93
|
+
ops_summary = []
|
94
|
+
# 统计操作类型
|
95
|
+
op_types = {}
|
96
|
+
for op in value:
|
97
|
+
if isinstance(op, dict):
|
98
|
+
for op_type in ['edit', 'create', 'update', 'delete', 'clear']:
|
99
|
+
if op_type in op:
|
100
|
+
op_types[op_type] = op_types.get(op_type, 0) + 1
|
101
|
+
|
102
|
+
# 显示前3个操作
|
103
|
+
for i in range(min(3, len(value))):
|
104
|
+
ops_summary.append(_sanitize_request_data(value[i], max_string_length, max_binary_preview))
|
105
|
+
|
106
|
+
# 添加统计信息
|
107
|
+
ops_summary.append(f"... 总计 {len(value)} 个操作: {', '.join(f'{k}={v}' for k, v in op_types.items())}")
|
108
|
+
result[key] = ops_summary
|
109
|
+
else:
|
110
|
+
result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
|
111
|
+
elif key.lower() in ['content', 'data', 'file', 'file_content', 'binary', 'blob', 'bytes', 'image', 'attachment']:
|
112
|
+
if isinstance(value, (bytes, bytearray)):
|
113
|
+
# 二进制内容,显示长度和预览
|
114
|
+
preview = base64.b64encode(value[:max_binary_preview]).decode('utf-8')
|
115
|
+
result[key] = f"<binary {len(value)} bytes, preview: {preview}...>"
|
116
|
+
elif isinstance(value, str):
|
117
|
+
# 检查是否是base64字符串
|
118
|
+
if _is_base64_string(value):
|
119
|
+
result[key] = f"<base64 string, length: {len(value)}, preview: {value[:max_binary_preview]}...>"
|
120
|
+
elif len(value) > max_string_length:
|
121
|
+
result[key] = _truncate_long_string(value, max_string_length)
|
122
|
+
else:
|
123
|
+
result[key] = value
|
124
|
+
else:
|
125
|
+
result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
|
126
|
+
else:
|
127
|
+
result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
|
128
|
+
return result
|
129
|
+
elif isinstance(data, list):
|
130
|
+
# 递归处理列表,限制列表长度以避免日志过长
|
131
|
+
max_list_items = 10 # 最多显示10个元素
|
132
|
+
if len(data) > max_list_items:
|
133
|
+
# 显示前5个和后5个元素
|
134
|
+
preview_items = (
|
135
|
+
[_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data[:5]] +
|
136
|
+
[f"... {len(data) - max_list_items} more items ..."] +
|
137
|
+
[_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data[-5:]]
|
138
|
+
)
|
139
|
+
return preview_items
|
140
|
+
else:
|
141
|
+
return [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data]
|
142
|
+
elif isinstance(data, tuple):
|
143
|
+
# 递归处理元组
|
144
|
+
return tuple(_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data)
|
145
|
+
elif isinstance(data, (bytes, bytearray)):
|
146
|
+
# 二进制内容
|
147
|
+
preview = base64.b64encode(data[:max_binary_preview]).decode('utf-8')
|
148
|
+
return f"<binary {len(data)} bytes, preview: {preview}...>"
|
149
|
+
elif isinstance(data, str):
|
150
|
+
# 字符串内容
|
151
|
+
if _is_base64_string(data):
|
152
|
+
return f"<base64 string, length: {len(data)}, preview: {data[:max_binary_preview]}...>"
|
153
|
+
elif len(data) > max_string_length:
|
154
|
+
return _truncate_long_string(data, max_string_length)
|
155
|
+
else:
|
156
|
+
return data
|
157
|
+
else:
|
158
|
+
# 其他类型直接返回
|
159
|
+
return data
|
160
|
+
|
161
|
+
|
162
|
+
class LoggingInterceptor:
|
163
|
+
"""gRPC日志拦截器基类"""
|
164
|
+
|
165
|
+
def __init__(self):
|
166
|
+
self.logger = get_logger()
|
167
|
+
self.request_logger = GrpcRequestLogger(self.logger)
|
168
|
+
|
169
|
+
|
170
|
+
class AsyncUnaryUnaryLoggingInterceptor(LoggingInterceptor, aio.UnaryUnaryClientInterceptor):
|
171
|
+
"""异步一元-一元gRPC日志拦截器"""
|
172
|
+
|
173
|
+
async def intercept_unary_unary(
|
174
|
+
self,
|
175
|
+
continuation: Callable,
|
176
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
177
|
+
request: Any
|
178
|
+
) -> Any:
|
179
|
+
"""拦截一元-一元调用"""
|
180
|
+
# 安全提取方法名
|
181
|
+
try:
|
182
|
+
method = client_call_details.method
|
183
|
+
if isinstance(method, bytes):
|
184
|
+
method = method.decode('utf-8', errors='ignore')
|
185
|
+
method_name = method.split('/')[-1] if '/' in method else method
|
186
|
+
except:
|
187
|
+
method_name = "unknown"
|
188
|
+
|
189
|
+
# 安全提取request_id和metadata
|
190
|
+
request_id = "unknown"
|
191
|
+
metadata_dict = {}
|
192
|
+
try:
|
193
|
+
if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
|
194
|
+
metadata_dict = _metadata_to_dict(client_call_details.metadata)
|
195
|
+
request_id = metadata_dict.get('x-request-id', 'unknown')
|
196
|
+
except:
|
197
|
+
pass
|
198
|
+
|
199
|
+
start_time = time.time()
|
200
|
+
|
201
|
+
# 记录请求开始
|
202
|
+
if self.logger.handlers:
|
203
|
+
try:
|
204
|
+
# 安全地提取请求数据
|
205
|
+
request_data = None
|
206
|
+
try:
|
207
|
+
if hasattr(request, '__class__'):
|
208
|
+
# 将protobuf消息转换为字典
|
209
|
+
from google.protobuf.json_format import MessageToDict
|
210
|
+
request_data = MessageToDict(request, preserving_proto_field_name=True)
|
211
|
+
# 清理请求数据,截断长内容
|
212
|
+
request_data = _sanitize_request_data(request_data)
|
213
|
+
except:
|
214
|
+
# 如果转换失败,尝试其他方法
|
215
|
+
try:
|
216
|
+
request_data = str(request)
|
217
|
+
if len(request_data) > 200:
|
218
|
+
request_data = _truncate_long_string(request_data)
|
219
|
+
except:
|
220
|
+
request_data = "<无法序列化>"
|
221
|
+
|
222
|
+
self.request_logger.log_request_start(
|
223
|
+
method_name, request_id, metadata_dict, request_data
|
224
|
+
)
|
225
|
+
except:
|
226
|
+
pass
|
227
|
+
|
228
|
+
try:
|
229
|
+
# 执行实际的gRPC调用
|
230
|
+
response = await continuation(client_call_details, request)
|
231
|
+
|
232
|
+
# 记录成功
|
233
|
+
duration_ms = (time.time() - start_time) * 1000
|
234
|
+
if self.logger.handlers:
|
235
|
+
try:
|
236
|
+
self.request_logger.log_request_end(
|
237
|
+
method_name, request_id, duration_ms, None, metadata=metadata_dict
|
238
|
+
)
|
239
|
+
except:
|
240
|
+
pass
|
241
|
+
|
242
|
+
return response
|
243
|
+
|
244
|
+
except Exception as e:
|
245
|
+
# 记录错误
|
246
|
+
duration_ms = (time.time() - start_time) * 1000
|
247
|
+
if self.logger.handlers:
|
248
|
+
try:
|
249
|
+
self.request_logger.log_request_end(
|
250
|
+
method_name, request_id, duration_ms, error=e, metadata=metadata_dict
|
251
|
+
)
|
252
|
+
except:
|
253
|
+
pass
|
254
|
+
raise
|
255
|
+
|
256
|
+
|
257
|
+
class AsyncUnaryStreamLoggingInterceptor(LoggingInterceptor, aio.UnaryStreamClientInterceptor):
|
258
|
+
"""异步一元-流gRPC日志拦截器"""
|
259
|
+
|
260
|
+
async def intercept_unary_stream(
|
261
|
+
self,
|
262
|
+
continuation: Callable,
|
263
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
264
|
+
request: Any
|
265
|
+
) -> Any:
|
266
|
+
"""拦截一元-流调用"""
|
267
|
+
# 安全提取方法名
|
268
|
+
try:
|
269
|
+
method = client_call_details.method
|
270
|
+
if isinstance(method, bytes):
|
271
|
+
method = method.decode('utf-8', errors='ignore')
|
272
|
+
method_name = method.split('/')[-1] if '/' in method else method
|
273
|
+
except:
|
274
|
+
method_name = "unknown"
|
275
|
+
|
276
|
+
# 安全提取request_id
|
277
|
+
request_id = "unknown"
|
278
|
+
try:
|
279
|
+
if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
|
280
|
+
for key, value in client_call_details.metadata:
|
281
|
+
if key == 'x-request-id':
|
282
|
+
request_id = value
|
283
|
+
break
|
284
|
+
except:
|
285
|
+
pass
|
286
|
+
|
287
|
+
start_time = time.time()
|
288
|
+
|
289
|
+
# 记录请求开始
|
290
|
+
if self.logger.handlers:
|
291
|
+
try:
|
292
|
+
# 安全地提取请求数据
|
293
|
+
request_data = None
|
294
|
+
try:
|
295
|
+
if hasattr(request, '__class__'):
|
296
|
+
from google.protobuf.json_format import MessageToDict
|
297
|
+
request_data = MessageToDict(request, preserving_proto_field_name=True)
|
298
|
+
# 清理请求数据,截断长内容
|
299
|
+
request_data = _sanitize_request_data(request_data)
|
300
|
+
except:
|
301
|
+
try:
|
302
|
+
request_data = str(request)
|
303
|
+
if len(request_data) > 200:
|
304
|
+
request_data = _truncate_long_string(request_data)
|
305
|
+
except:
|
306
|
+
request_data = "<无法序列化>"
|
307
|
+
|
308
|
+
self.request_logger.log_request_start(
|
309
|
+
method_name, request_id, {}, request_data
|
310
|
+
)
|
311
|
+
except:
|
312
|
+
pass
|
313
|
+
|
314
|
+
try:
|
315
|
+
# 执行实际的gRPC调用
|
316
|
+
response_stream = await continuation(client_call_details, request)
|
317
|
+
|
318
|
+
# 记录流开始
|
319
|
+
duration_ms = (time.time() - start_time) * 1000
|
320
|
+
if self.logger.handlers:
|
321
|
+
try:
|
322
|
+
self.request_logger.log_request_end(
|
323
|
+
method_name, request_id, duration_ms, "<Stream started>"
|
324
|
+
)
|
325
|
+
except:
|
326
|
+
pass
|
327
|
+
|
328
|
+
return response_stream
|
329
|
+
|
330
|
+
except Exception as e:
|
331
|
+
# 记录请求错误
|
332
|
+
duration_ms = (time.time() - start_time) * 1000
|
333
|
+
if self.logger.handlers:
|
334
|
+
try:
|
335
|
+
self.request_logger.log_request_end(
|
336
|
+
method_name, request_id, duration_ms, error=e
|
337
|
+
)
|
338
|
+
except:
|
339
|
+
pass
|
340
|
+
raise
|
341
|
+
|
342
|
+
|
343
|
+
class AsyncStreamUnaryLoggingInterceptor(LoggingInterceptor, aio.StreamUnaryClientInterceptor):
|
344
|
+
"""异步流-一元gRPC日志拦截器"""
|
345
|
+
|
346
|
+
async def intercept_stream_unary(
|
347
|
+
self,
|
348
|
+
continuation: Callable,
|
349
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
350
|
+
request_iterator: Any
|
351
|
+
) -> Any:
|
352
|
+
"""拦截流-一元调用"""
|
353
|
+
# 安全提取方法名
|
354
|
+
try:
|
355
|
+
method = client_call_details.method
|
356
|
+
if isinstance(method, bytes):
|
357
|
+
method = method.decode('utf-8', errors='ignore')
|
358
|
+
method_name = method.split('/')[-1] if '/' in method else method
|
359
|
+
except:
|
360
|
+
method_name = "unknown"
|
361
|
+
|
362
|
+
# 安全提取request_id
|
363
|
+
request_id = "unknown"
|
364
|
+
try:
|
365
|
+
if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
|
366
|
+
for key, value in client_call_details.metadata:
|
367
|
+
if key == 'x-request-id':
|
368
|
+
request_id = value
|
369
|
+
break
|
370
|
+
except:
|
371
|
+
pass
|
372
|
+
|
373
|
+
start_time = time.time()
|
374
|
+
|
375
|
+
# 记录请求开始
|
376
|
+
if self.logger.handlers:
|
377
|
+
try:
|
378
|
+
self.request_logger.log_request_start(
|
379
|
+
method_name, request_id, {}, "<Stream request>"
|
380
|
+
)
|
381
|
+
except:
|
382
|
+
pass
|
383
|
+
|
384
|
+
try:
|
385
|
+
# 执行实际的gRPC调用
|
386
|
+
response = await continuation(client_call_details, request_iterator)
|
387
|
+
|
388
|
+
# 记录请求成功结束
|
389
|
+
duration_ms = (time.time() - start_time) * 1000
|
390
|
+
if self.logger.handlers:
|
391
|
+
try:
|
392
|
+
self.request_logger.log_request_end(
|
393
|
+
method_name, request_id, duration_ms, None
|
394
|
+
)
|
395
|
+
except:
|
396
|
+
pass
|
397
|
+
|
398
|
+
return response
|
399
|
+
|
400
|
+
except Exception as e:
|
401
|
+
# 记录请求错误
|
402
|
+
duration_ms = (time.time() - start_time) * 1000
|
403
|
+
if self.logger.handlers:
|
404
|
+
try:
|
405
|
+
self.request_logger.log_request_end(
|
406
|
+
method_name, request_id, duration_ms, error=e
|
407
|
+
)
|
408
|
+
except:
|
409
|
+
pass
|
410
|
+
raise
|
411
|
+
|
412
|
+
|
413
|
+
class AsyncStreamStreamLoggingInterceptor(LoggingInterceptor, aio.StreamStreamClientInterceptor):
|
414
|
+
"""异步流-流gRPC日志拦截器"""
|
415
|
+
|
416
|
+
async def intercept_stream_stream(
|
417
|
+
self,
|
418
|
+
continuation: Callable,
|
419
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
420
|
+
request_iterator: Any
|
421
|
+
) -> Any:
|
422
|
+
"""拦截流-流调用"""
|
423
|
+
# 安全提取方法名
|
424
|
+
try:
|
425
|
+
method = client_call_details.method
|
426
|
+
if isinstance(method, bytes):
|
427
|
+
method = method.decode('utf-8', errors='ignore')
|
428
|
+
method_name = method.split('/')[-1] if '/' in method else method
|
429
|
+
except:
|
430
|
+
method_name = "unknown"
|
431
|
+
|
432
|
+
# 安全提取request_id
|
433
|
+
request_id = "unknown"
|
434
|
+
try:
|
435
|
+
if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
|
436
|
+
for key, value in client_call_details.metadata:
|
437
|
+
if key == 'x-request-id':
|
438
|
+
request_id = value
|
439
|
+
break
|
440
|
+
except:
|
441
|
+
pass
|
442
|
+
|
443
|
+
start_time = time.time()
|
444
|
+
|
445
|
+
# 记录请求开始
|
446
|
+
if self.logger.handlers:
|
447
|
+
try:
|
448
|
+
self.request_logger.log_request_start(
|
449
|
+
method_name, request_id, {}, "<Stream request>"
|
450
|
+
)
|
451
|
+
except:
|
452
|
+
pass
|
453
|
+
|
454
|
+
try:
|
455
|
+
# 执行实际的gRPC调用
|
456
|
+
response_stream = await continuation(client_call_details, request_iterator)
|
457
|
+
|
458
|
+
# 记录流开始
|
459
|
+
duration_ms = (time.time() - start_time) * 1000
|
460
|
+
if self.logger.handlers:
|
461
|
+
try:
|
462
|
+
self.request_logger.log_request_end(
|
463
|
+
method_name, request_id, duration_ms, "<Stream started>"
|
464
|
+
)
|
465
|
+
except:
|
466
|
+
pass
|
467
|
+
|
468
|
+
return response_stream
|
469
|
+
|
470
|
+
except Exception as e:
|
471
|
+
# 记录请求错误
|
472
|
+
duration_ms = (time.time() - start_time) * 1000
|
473
|
+
if self.logger.handlers:
|
474
|
+
try:
|
475
|
+
self.request_logger.log_request_end(
|
476
|
+
method_name, request_id, duration_ms, error=e
|
477
|
+
)
|
478
|
+
except:
|
479
|
+
pass
|
480
|
+
raise
|
481
|
+
|
482
|
+
|
483
|
+
class SyncUnaryUnaryLoggingInterceptor(grpc.UnaryUnaryClientInterceptor):
|
484
|
+
"""同步一元-一元gRPC日志拦截器"""
|
485
|
+
|
486
|
+
def intercept_unary_unary(self, continuation, client_call_details, request):
|
487
|
+
logger = get_logger()
|
488
|
+
request_logger = GrpcRequestLogger(logger)
|
489
|
+
|
490
|
+
# 安全提取方法名
|
491
|
+
try:
|
492
|
+
method = client_call_details.method
|
493
|
+
if isinstance(method, bytes):
|
494
|
+
method = method.decode('utf-8', errors='ignore')
|
495
|
+
method_name = method.split('/')[-1] if '/' in method else method
|
496
|
+
except:
|
497
|
+
method_name = "unknown"
|
498
|
+
|
499
|
+
# 安全提取request_id和metadata
|
500
|
+
request_id = "unknown"
|
501
|
+
metadata_dict = {}
|
502
|
+
try:
|
503
|
+
if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
|
504
|
+
metadata_dict = _metadata_to_dict(client_call_details.metadata)
|
505
|
+
request_id = metadata_dict.get('x-request-id', 'unknown')
|
506
|
+
except:
|
507
|
+
pass
|
508
|
+
|
509
|
+
start_time = time.time()
|
510
|
+
|
511
|
+
# 记录请求开始
|
512
|
+
if logger.handlers:
|
513
|
+
try:
|
514
|
+
# 安全地提取请求数据
|
515
|
+
request_data = None
|
516
|
+
try:
|
517
|
+
if hasattr(request, '__class__'):
|
518
|
+
from google.protobuf.json_format import MessageToDict
|
519
|
+
request_data = MessageToDict(request, preserving_proto_field_name=True)
|
520
|
+
# 清理请求数据,截断长内容
|
521
|
+
request_data = _sanitize_request_data(request_data)
|
522
|
+
except:
|
523
|
+
try:
|
524
|
+
request_data = str(request)
|
525
|
+
if len(request_data) > 200:
|
526
|
+
request_data = _truncate_long_string(request_data)
|
527
|
+
except:
|
528
|
+
request_data = "<无法序列化>"
|
529
|
+
|
530
|
+
request_logger.log_request_start(
|
531
|
+
method_name, request_id, metadata_dict, request_data
|
532
|
+
)
|
533
|
+
except:
|
534
|
+
pass
|
535
|
+
|
536
|
+
try:
|
537
|
+
# 执行实际的gRPC调用
|
538
|
+
response = continuation(client_call_details, request)
|
539
|
+
|
540
|
+
# 记录请求成功结束
|
541
|
+
duration_ms = (time.time() - start_time) * 1000
|
542
|
+
if logger.handlers:
|
543
|
+
try:
|
544
|
+
request_logger.log_request_end(
|
545
|
+
method_name, request_id, duration_ms, None, metadata=metadata_dict
|
546
|
+
)
|
547
|
+
except:
|
548
|
+
pass
|
549
|
+
|
550
|
+
return response
|
551
|
+
|
552
|
+
except Exception as e:
|
553
|
+
# 记录请求错误
|
554
|
+
duration_ms = (time.time() - start_time) * 1000
|
555
|
+
if logger.handlers:
|
556
|
+
try:
|
557
|
+
request_logger.log_request_end(
|
558
|
+
method_name, request_id, duration_ms, error=e, metadata=metadata_dict
|
559
|
+
)
|
560
|
+
except:
|
561
|
+
pass
|
562
|
+
raise
|
563
|
+
|
564
|
+
|
565
|
+
def create_async_interceptors() -> List[aio.ClientInterceptor]:
|
566
|
+
"""创建异步gRPC拦截器列表"""
|
567
|
+
return [
|
568
|
+
AsyncUnaryUnaryLoggingInterceptor(),
|
569
|
+
AsyncUnaryStreamLoggingInterceptor(),
|
570
|
+
AsyncStreamUnaryLoggingInterceptor(),
|
571
|
+
AsyncStreamStreamLoggingInterceptor(),
|
572
|
+
]
|
573
|
+
|
574
|
+
|
575
|
+
def create_sync_interceptors() -> List:
|
576
|
+
"""创建同步gRPC拦截器列表"""
|
577
|
+
return [
|
578
|
+
SyncUnaryUnaryLoggingInterceptor(),
|
581
579
|
]
|