tamar-file-hub-client 0.1.4__py3-none-any.whl → 0.1.6__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.
@@ -1,319 +1,336 @@
1
- """
2
- 日志配置和工具
3
- """
4
- import logging
5
- import sys
6
- import time
7
- import json
8
- import traceback
9
- from typing import Optional, Any, Dict
10
- from functools import wraps
11
- from contextlib import contextmanager
12
- from datetime import datetime
13
-
14
- # 创建SDK专用的日志记录器 - 使用独立的命名空间避免冲突
15
- SDK_LOGGER_NAME = "file_hub_client.grpc"
16
- logger = logging.getLogger(SDK_LOGGER_NAME)
17
-
18
-
19
- class GrpcJSONFormatter(logging.Formatter):
20
- """gRPC请求的JSON格式化器"""
21
-
22
- def format(self, record):
23
- log_type = getattr(record, "log_type", "info")
24
- log_data = {
25
- "timestamp": datetime.fromtimestamp(record.created).isoformat(),
26
- "level": record.levelname,
27
- "type": log_type,
28
- "uri": getattr(record, "uri", None),
29
- "request_id": getattr(record, "request_id", None),
30
- "data": getattr(record, "data", None),
31
- "message": record.getMessage(),
32
- "duration": getattr(record, "duration", None),
33
- "logger": record.name, # 添加logger名称以区分SDK日志
34
- }
35
-
36
- # 增加 trace 支持
37
- if hasattr(record, "trace"):
38
- log_data["trace"] = getattr(record, "trace")
39
-
40
- # 添加异常信息(如果有的话)
41
- if hasattr(record, "exc_info") and record.exc_info:
42
- log_data["exception"] = {
43
- "type": record.exc_info[0].__name__ if record.exc_info[0] else None,
44
- "message": str(record.exc_info[1]) if record.exc_info[1] else None,
45
- "traceback": traceback.format_exception(*record.exc_info)
46
- }
47
-
48
- # 过滤掉None值
49
- log_data = {k: v for k, v in log_data.items() if v is not None}
50
-
51
- return json.dumps(log_data, ensure_ascii=False)
52
-
53
-
54
- def get_default_formatter() -> logging.Formatter:
55
- """获取默认的JSON格式化器"""
56
- return GrpcJSONFormatter()
57
-
58
-
59
- def setup_logging(
60
- level: str = "INFO",
61
- format_string: Optional[str] = None,
62
- enable_grpc_logging: bool = True,
63
- log_request_payload: bool = False,
64
- log_response_payload: bool = False,
65
- handler: Optional[logging.Handler] = None,
66
- use_json_format: bool = True
67
- ):
68
- """
69
- 设置SDK日志记录配置
70
-
71
- Args:
72
- level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
73
- format_string: 自定义日志格式(当use_json_format=False时使用)
74
- enable_grpc_logging: 是否启用gRPC请求日志
75
- log_request_payload: 是否记录请求载荷
76
- log_response_payload: 是否记录响应载荷
77
- handler: 自定义日志处理器
78
- use_json_format: 是否使用JSON格式(默认True)
79
- """
80
- # 设置日志级别
81
- log_level = getattr(logging, level.upper(), logging.INFO)
82
- logger.setLevel(log_level)
83
-
84
- # 清除现有的处理器(只清除SDK的logger)
85
- logger.handlers.clear()
86
-
87
- # 创建处理器
88
- if handler is None:
89
- handler = logging.StreamHandler(sys.stdout)
90
-
91
- # 设置日志格式
92
- if use_json_format:
93
- formatter = get_default_formatter()
94
- else:
95
- if format_string is None:
96
- format_string = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s"
97
- formatter = logging.Formatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
98
-
99
- handler.setFormatter(formatter)
100
-
101
- # 添加处理器
102
- logger.addHandler(handler)
103
-
104
- # 设置gRPC日志配置
105
- logger.grpc_logging_enabled = enable_grpc_logging
106
- logger.log_request_payload = log_request_payload
107
- logger.log_response_payload = log_response_payload
108
-
109
- # 防止日志传播到根日志记录器 - 保持SDK日志独立
110
- logger.propagate = False
111
-
112
- # 对整个 file_hub_client 包设置隔离,确保所有子模块的日志都不会传播
113
- parent_logger = logging.getLogger('file_hub_client')
114
- parent_logger.propagate = False
115
-
116
- # 初始化日志(使用JSON格式)
117
- if enable_grpc_logging:
118
- log_record = logging.LogRecord(
119
- name=logger.name,
120
- level=logging.INFO,
121
- pathname="",
122
- lineno=0,
123
- msg="📡 文件中心客户端 gRPC 日志已初始化",
124
- args=(),
125
- exc_info=None
126
- )
127
- log_record.log_type = "info"
128
- log_record.data = {
129
- "level": level,
130
- "grpc_logging": enable_grpc_logging,
131
- "json_format": use_json_format
132
- }
133
- logger.handle(log_record)
134
-
135
-
136
- def get_logger() -> logging.Logger:
137
- """获取SDK日志记录器"""
138
- return logger
139
-
140
-
141
- class GrpcRequestLogger:
142
- """gRPC请求日志记录器"""
143
-
144
- def __init__(self, logger: logging.Logger):
145
- self.logger = logger
146
- self.enable_grpc_logging = getattr(logger, 'grpc_logging_enabled', True)
147
- self.log_request_payload = getattr(logger, 'log_request_payload', False)
148
- self.log_response_payload = getattr(logger, 'log_response_payload', False)
149
-
150
- def log_request_start(self, method_name: str, request_id: str, metadata: Dict[str, Any],
151
- request_payload: Any = None):
152
- """记录请求开始"""
153
- if not self.enable_grpc_logging:
154
- return
155
-
156
- # 提取关键元数据
157
- user_info = {}
158
- if metadata:
159
- metadata_dict = dict(metadata) if isinstance(metadata, list) else metadata
160
- user_info = {
161
- 'org_id': metadata_dict.get('x-org-id'),
162
- 'user_id': metadata_dict.get('x-user-id'),
163
- 'client_ip': metadata_dict.get('x-client-ip'),
164
- 'client_version': metadata_dict.get('x-client-version')
165
- }
166
- user_info = {k: v for k, v in user_info.items() if v is not None}
167
-
168
- # 创建日志记录
169
- log_record = logging.LogRecord(
170
- name=self.logger.name,
171
- level=logging.INFO,
172
- pathname="",
173
- lineno=0,
174
- msg=f"📤 gRPC 请求: {method_name}",
175
- args=(),
176
- exc_info=None
177
- )
178
-
179
- # 添加自定义字段
180
- log_record.log_type = "request"
181
- log_record.uri = method_name
182
- log_record.request_id = request_id
183
- log_record.data = user_info
184
-
185
- # 记录请求载荷
186
- if request_payload is not None:
187
- if isinstance(request_payload, dict):
188
- # 已经是字典格式,直接合并
189
- log_record.data.update(request_payload)
190
- else:
191
- # 其他格式,添加到payload字段
192
- log_record.data["payload"] = request_payload
193
-
194
- self.logger.handle(log_record)
195
-
196
- def log_request_end(self, method_name: str, request_id: str, duration_ms: float,
197
- response_payload: Any = None, error: Exception = None):
198
- """记录请求结束"""
199
- if not self.enable_grpc_logging:
200
- return
201
-
202
- if error:
203
- # 错误日志
204
- log_record = logging.LogRecord(
205
- name=self.logger.name,
206
- level=logging.ERROR,
207
- pathname="",
208
- lineno=0,
209
- msg=f"❌ gRPC 错误: {method_name} - {str(error)}",
210
- args=(),
211
- exc_info=(type(error), error, error.__traceback__) if error else None
212
- )
213
- log_record.log_type = "error"
214
- log_record.uri = method_name
215
- log_record.request_id = request_id
216
- log_record.duration = duration_ms
217
- log_record.data = {"error": str(error)}
218
-
219
- self.logger.handle(log_record)
220
- else:
221
- # 响应日志
222
- log_record = logging.LogRecord(
223
- name=self.logger.name,
224
- level=logging.INFO,
225
- pathname="",
226
- lineno=0,
227
- msg=f"✅ gRPC 响应: {method_name}",
228
- args=(),
229
- exc_info=None
230
- )
231
- log_record.log_type = "response"
232
- log_record.uri = method_name
233
- log_record.request_id = request_id
234
- log_record.duration = duration_ms
235
-
236
- # 记录响应载荷(如果启用)
237
- if self.log_response_payload and response_payload is not None:
238
- log_record.data = {"response_payload": self._safe_serialize(response_payload)}
239
-
240
- self.logger.handle(log_record)
241
-
242
- def _safe_serialize(self, obj: Any) -> str:
243
- """安全地序列化对象,避免敏感信息泄露"""
244
- try:
245
- if hasattr(obj, 'SerializeToString'):
246
- # protobuf 对象
247
- return f"<Proto object: {type(obj).__name__}>"
248
- elif hasattr(obj, '__dict__'):
249
- # 普通对象
250
- return f"<Object: {type(obj).__name__}>"
251
- else:
252
- # 基本类型
253
- return str(obj)[:200] # 限制长度
254
- except Exception:
255
- return f"<Unserializable: {type(obj).__name__}>"
256
-
257
-
258
- @contextmanager
259
- def grpc_request_context(method_name: str, request_id: str, metadata: Dict[str, Any],
260
- request_payload: Any = None):
261
- """gRPC请求上下文管理器"""
262
- request_logger = GrpcRequestLogger(get_logger())
263
- start_time = time.time()
264
-
265
- try:
266
- # 记录请求开始
267
- request_logger.log_request_start(method_name, request_id, metadata, request_payload)
268
- yield request_logger
269
-
270
- except Exception as e:
271
- # 记录请求错误
272
- duration_ms = (time.time() - start_time) * 1000
273
- request_logger.log_request_end(method_name, request_id, duration_ms, error=e)
274
- raise
275
-
276
- else:
277
- # 记录请求成功结束
278
- duration_ms = (time.time() - start_time) * 1000
279
- request_logger.log_request_end(method_name, request_id, duration_ms)
280
-
281
-
282
- def log_grpc_call(method_name: str):
283
- """gRPC调用日志装饰器"""
284
- def decorator(func):
285
- @wraps(func)
286
- def sync_wrapper(*args, **kwargs):
287
- # 提取request_id和metadata
288
- request_id = kwargs.get('request_id', 'unknown')
289
- metadata = kwargs.get('metadata', {})
290
-
291
- with grpc_request_context(method_name, request_id, metadata) as request_logger:
292
- result = func(*args, **kwargs)
293
- request_logger.log_request_end(method_name, request_id, 0, response_payload=result)
294
- return result
295
-
296
- @wraps(func)
297
- async def async_wrapper(*args, **kwargs):
298
- # 提取request_id和metadata
299
- request_id = kwargs.get('request_id', 'unknown')
300
- metadata = kwargs.get('metadata', {})
301
-
302
- with grpc_request_context(method_name, request_id, metadata) as request_logger:
303
- result = await func(*args, **kwargs)
304
- request_logger.log_request_end(method_name, request_id, 0, response_payload=result)
305
- return result
306
-
307
- # 根据函数类型返回对应的包装器
308
- import asyncio
309
- if asyncio.iscoroutinefunction(func):
310
- return async_wrapper
311
- else:
312
- return sync_wrapper
313
-
314
- return decorator
315
-
316
-
317
- # 默认初始化(可以被用户重新配置)
318
- if not logger.handlers:
1
+ """
2
+ 日志配置和工具
3
+ """
4
+ import logging
5
+ import sys
6
+ import time
7
+ import json
8
+ import traceback
9
+ from typing import Optional, Any, Dict
10
+ from functools import wraps
11
+ from contextlib import contextmanager
12
+ from datetime import datetime
13
+
14
+ # 创建SDK专用的日志记录器 - 使用独立的命名空间避免冲突
15
+ SDK_LOGGER_NAME = "file_hub_client.grpc"
16
+ logger = logging.getLogger(SDK_LOGGER_NAME)
17
+
18
+
19
+ class GrpcJSONFormatter(logging.Formatter):
20
+ """gRPC请求的JSON格式化器"""
21
+
22
+ def format(self, record):
23
+ log_type = getattr(record, "log_type", "info")
24
+ log_data = {
25
+ "timestamp": datetime.fromtimestamp(record.created).isoformat(),
26
+ "level": record.levelname,
27
+ "type": log_type,
28
+ "uri": getattr(record, "uri", None),
29
+ "request_id": getattr(record, "request_id", None),
30
+ "data": getattr(record, "data", None),
31
+ "message": record.getMessage(),
32
+ "duration": getattr(record, "duration", None),
33
+ "logger": record.name, # 添加logger名称以区分SDK日志
34
+ }
35
+
36
+ # 增加 trace 支持
37
+ if hasattr(record, "trace"):
38
+ log_data["trace"] = getattr(record, "trace")
39
+
40
+ # 添加异常信息(如果有的话)
41
+ if hasattr(record, "exc_info") and record.exc_info:
42
+ log_data["exception"] = {
43
+ "type": record.exc_info[0].__name__ if record.exc_info[0] else None,
44
+ "message": str(record.exc_info[1]) if record.exc_info[1] else None,
45
+ "traceback": traceback.format_exception(*record.exc_info)
46
+ }
47
+
48
+ # 过滤掉None值
49
+ log_data = {k: v for k, v in log_data.items() if v is not None}
50
+
51
+ return json.dumps(log_data, ensure_ascii=False)
52
+
53
+
54
+ def get_default_formatter() -> logging.Formatter:
55
+ """获取默认的JSON格式化器"""
56
+ return GrpcJSONFormatter()
57
+
58
+
59
+ def setup_logging(
60
+ level: str = "INFO",
61
+ format_string: Optional[str] = None,
62
+ enable_grpc_logging: bool = True,
63
+ log_request_payload: bool = False,
64
+ log_response_payload: bool = False,
65
+ handler: Optional[logging.Handler] = None,
66
+ use_json_format: bool = True
67
+ ):
68
+ """
69
+ 设置SDK日志记录配置
70
+
71
+ Args:
72
+ level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
73
+ format_string: 自定义日志格式(当use_json_format=False时使用)
74
+ enable_grpc_logging: 是否启用gRPC请求日志
75
+ log_request_payload: 是否记录请求载荷
76
+ log_response_payload: 是否记录响应载荷
77
+ handler: 自定义日志处理器
78
+ use_json_format: 是否使用JSON格式(默认True)
79
+ """
80
+ # 设置日志级别
81
+ log_level = getattr(logging, level.upper(), logging.INFO)
82
+ logger.setLevel(log_level)
83
+
84
+ # 清除现有的处理器(只清除SDK的logger)
85
+ logger.handlers.clear()
86
+
87
+ # 创建处理器
88
+ if handler is None:
89
+ handler = logging.StreamHandler(sys.stdout)
90
+
91
+ # 设置日志格式
92
+ if use_json_format:
93
+ formatter = get_default_formatter()
94
+ else:
95
+ if format_string is None:
96
+ format_string = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s"
97
+ formatter = logging.Formatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
98
+
99
+ handler.setFormatter(formatter)
100
+
101
+ # 添加处理器
102
+ logger.addHandler(handler)
103
+
104
+ # 设置gRPC日志配置
105
+ logger.grpc_logging_enabled = enable_grpc_logging
106
+ logger.log_request_payload = log_request_payload
107
+ logger.log_response_payload = log_response_payload
108
+
109
+ # 防止日志传播到根日志记录器 - 保持SDK日志独立
110
+ logger.propagate = False
111
+
112
+ # 对整个 file_hub_client 包设置隔离,确保所有子模块的日志都不会传播
113
+ parent_logger = logging.getLogger('file_hub_client')
114
+ parent_logger.propagate = False
115
+
116
+ # 初始化日志(使用JSON格式)
117
+ if enable_grpc_logging:
118
+ log_record = logging.LogRecord(
119
+ name=logger.name,
120
+ level=logging.INFO,
121
+ pathname="",
122
+ lineno=0,
123
+ msg="📡 文件中心客户端 gRPC 日志已初始化",
124
+ args=(),
125
+ exc_info=None
126
+ )
127
+ log_record.log_type = "info"
128
+ log_record.data = {
129
+ "level": level,
130
+ "grpc_logging": enable_grpc_logging,
131
+ "json_format": use_json_format
132
+ }
133
+ logger.handle(log_record)
134
+
135
+
136
+ def get_logger() -> logging.Logger:
137
+ """获取SDK日志记录器"""
138
+ return logger
139
+
140
+
141
+ class GrpcRequestLogger:
142
+ """gRPC请求日志记录器"""
143
+
144
+ def __init__(self, logger: logging.Logger):
145
+ self.logger = logger
146
+ self.enable_grpc_logging = getattr(logger, 'grpc_logging_enabled', True)
147
+ self.log_request_payload = getattr(logger, 'log_request_payload', False)
148
+ self.log_response_payload = getattr(logger, 'log_response_payload', False)
149
+
150
+ def log_request_start(self, method_name: str, request_id: str, metadata: Dict[str, Any],
151
+ request_payload: Any = None):
152
+ """记录请求开始"""
153
+ if not self.enable_grpc_logging:
154
+ return
155
+
156
+ # 提取关键元数据
157
+ user_info = {}
158
+ if metadata:
159
+ metadata_dict = dict(metadata) if isinstance(metadata, list) else metadata
160
+ user_info = {
161
+ 'org_id': metadata_dict.get('x-org-id'),
162
+ 'user_id': metadata_dict.get('x-user-id'),
163
+ 'client_ip': metadata_dict.get('x-client-ip'), # SDK客户端服务IP
164
+ 'user_ip': metadata_dict.get('x-user-ip'), # 用户真实IP
165
+ 'client_version': metadata_dict.get('x-client-version')
166
+ }
167
+ user_info = {k: v for k, v in user_info.items() if v is not None}
168
+
169
+ # 创建日志记录
170
+ log_record = logging.LogRecord(
171
+ name=self.logger.name,
172
+ level=logging.INFO,
173
+ pathname="",
174
+ lineno=0,
175
+ msg=f"📤 gRPC 请求: {method_name}",
176
+ args=(),
177
+ exc_info=None
178
+ )
179
+
180
+ # 添加自定义字段
181
+ log_record.log_type = "request"
182
+ log_record.uri = method_name
183
+ log_record.request_id = request_id
184
+ log_record.data = user_info
185
+
186
+ # 记录请求载荷
187
+ if request_payload is not None:
188
+ if isinstance(request_payload, dict):
189
+ # 已经是字典格式,直接合并
190
+ log_record.data.update(request_payload)
191
+ else:
192
+ # 其他格式,添加到payload字段
193
+ log_record.data["payload"] = request_payload
194
+
195
+ self.logger.handle(log_record)
196
+
197
+ def log_request_end(self, method_name: str, request_id: str, duration_ms: float,
198
+ response_payload: Any = None, error: Exception = None, metadata: Dict[str, Any] = None):
199
+ """记录请求结束"""
200
+ if not self.enable_grpc_logging:
201
+ return
202
+
203
+ if error:
204
+ # 错误日志
205
+ log_record = logging.LogRecord(
206
+ name=self.logger.name,
207
+ level=logging.ERROR,
208
+ pathname="",
209
+ lineno=0,
210
+ msg=f"❌ gRPC 错误: {method_name} - {str(error)}",
211
+ args=(),
212
+ exc_info=(type(error), error, error.__traceback__) if error else None
213
+ )
214
+ log_record.log_type = "error"
215
+ log_record.uri = method_name
216
+ log_record.request_id = request_id
217
+ log_record.duration = duration_ms
218
+ log_record.data = {"error": str(error)}
219
+
220
+ self.logger.handle(log_record)
221
+ else:
222
+ # 响应日志
223
+ log_record = logging.LogRecord(
224
+ name=self.logger.name,
225
+ level=logging.INFO,
226
+ pathname="",
227
+ lineno=0,
228
+ msg=f"✅ gRPC 响应: {method_name}",
229
+ args=(),
230
+ exc_info=None
231
+ )
232
+ log_record.log_type = "response"
233
+ log_record.uri = method_name
234
+ log_record.request_id = request_id
235
+ log_record.duration = duration_ms
236
+
237
+ # 初始化data字段用于存储metadata信息
238
+ log_record.data = {}
239
+
240
+ # 记录metadata信息
241
+ if metadata:
242
+ metadata_dict = dict(metadata) if isinstance(metadata, list) else metadata
243
+ user_info = {
244
+ 'org_id': metadata_dict.get('x-org-id'),
245
+ 'user_id': metadata_dict.get('x-user-id'),
246
+ 'client_ip': metadata_dict.get('x-client-ip'), # SDK客户端服务IP
247
+ 'user_ip': metadata_dict.get('x-user-ip'), # 用户真实IP
248
+ 'client_version': metadata_dict.get('x-client-version')
249
+ }
250
+ user_info = {k: v for k, v in user_info.items() if v is not None}
251
+ log_record.data.update(user_info)
252
+
253
+ # 记录响应载荷(如果启用)
254
+ if self.log_response_payload and response_payload is not None:
255
+ log_record.data["response_payload"] = self._safe_serialize(response_payload)
256
+
257
+ self.logger.handle(log_record)
258
+
259
+ def _safe_serialize(self, obj: Any) -> str:
260
+ """安全地序列化对象,避免敏感信息泄露"""
261
+ try:
262
+ if hasattr(obj, 'SerializeToString'):
263
+ # protobuf 对象
264
+ return f"<Proto object: {type(obj).__name__}>"
265
+ elif hasattr(obj, '__dict__'):
266
+ # 普通对象
267
+ return f"<Object: {type(obj).__name__}>"
268
+ else:
269
+ # 基本类型
270
+ return str(obj)[:200] # 限制长度
271
+ except Exception:
272
+ return f"<Unserializable: {type(obj).__name__}>"
273
+
274
+
275
+ @contextmanager
276
+ def grpc_request_context(method_name: str, request_id: str, metadata: Dict[str, Any],
277
+ request_payload: Any = None):
278
+ """gRPC请求上下文管理器"""
279
+ request_logger = GrpcRequestLogger(get_logger())
280
+ start_time = time.time()
281
+
282
+ try:
283
+ # 记录请求开始
284
+ request_logger.log_request_start(method_name, request_id, metadata, request_payload)
285
+ yield request_logger
286
+
287
+ except Exception as e:
288
+ # 记录请求错误
289
+ duration_ms = (time.time() - start_time) * 1000
290
+ request_logger.log_request_end(method_name, request_id, duration_ms, error=e, metadata=metadata)
291
+ raise
292
+
293
+ else:
294
+ # 记录请求成功结束
295
+ duration_ms = (time.time() - start_time) * 1000
296
+ request_logger.log_request_end(method_name, request_id, duration_ms, metadata=metadata)
297
+
298
+
299
+ def log_grpc_call(method_name: str):
300
+ """gRPC调用日志装饰器"""
301
+ def decorator(func):
302
+ @wraps(func)
303
+ def sync_wrapper(*args, **kwargs):
304
+ # 提取request_id和metadata
305
+ request_id = kwargs.get('request_id', 'unknown')
306
+ metadata = kwargs.get('metadata', {})
307
+
308
+ with grpc_request_context(method_name, request_id, metadata) as request_logger:
309
+ result = func(*args, **kwargs)
310
+ request_logger.log_request_end(method_name, request_id, 0, response_payload=result, metadata=metadata)
311
+ return result
312
+
313
+ @wraps(func)
314
+ async def async_wrapper(*args, **kwargs):
315
+ # 提取request_id和metadata
316
+ request_id = kwargs.get('request_id', 'unknown')
317
+ metadata = kwargs.get('metadata', {})
318
+
319
+ with grpc_request_context(method_name, request_id, metadata) as request_logger:
320
+ result = await func(*args, **kwargs)
321
+ request_logger.log_request_end(method_name, request_id, 0, response_payload=result, metadata=metadata)
322
+ return result
323
+
324
+ # 根据函数类型返回对应的包装器
325
+ import asyncio
326
+ if asyncio.iscoroutinefunction(func):
327
+ return async_wrapper
328
+ else:
329
+ return sync_wrapper
330
+
331
+ return decorator
332
+
333
+
334
+ # 默认初始化(可以被用户重新配置)
335
+ if not logger.handlers:
319
336
  setup_logging()