suprema-biostar-mcp 1.0.1__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.
Files changed (61) hide show
  1. biostar_x_mcp_server/__init__.py +25 -0
  2. biostar_x_mcp_server/__main__.py +15 -0
  3. biostar_x_mcp_server/config.py +87 -0
  4. biostar_x_mcp_server/handlers/__init__.py +35 -0
  5. biostar_x_mcp_server/handlers/access_handler.py +2162 -0
  6. biostar_x_mcp_server/handlers/audit_handler.py +489 -0
  7. biostar_x_mcp_server/handlers/auth_handler.py +216 -0
  8. biostar_x_mcp_server/handlers/base_handler.py +228 -0
  9. biostar_x_mcp_server/handlers/card_handler.py +746 -0
  10. biostar_x_mcp_server/handlers/device_handler.py +4344 -0
  11. biostar_x_mcp_server/handlers/door_handler.py +3969 -0
  12. biostar_x_mcp_server/handlers/event_handler.py +1331 -0
  13. biostar_x_mcp_server/handlers/file_handler.py +212 -0
  14. biostar_x_mcp_server/handlers/help_web_handler.py +379 -0
  15. biostar_x_mcp_server/handlers/log_handler.py +1051 -0
  16. biostar_x_mcp_server/handlers/navigation_handler.py +109 -0
  17. biostar_x_mcp_server/handlers/occupancy_handler.py +541 -0
  18. biostar_x_mcp_server/handlers/user_handler.py +3568 -0
  19. biostar_x_mcp_server/schemas/__init__.py +21 -0
  20. biostar_x_mcp_server/schemas/access.py +158 -0
  21. biostar_x_mcp_server/schemas/audit.py +73 -0
  22. biostar_x_mcp_server/schemas/auth.py +24 -0
  23. biostar_x_mcp_server/schemas/cards.py +128 -0
  24. biostar_x_mcp_server/schemas/devices.py +496 -0
  25. biostar_x_mcp_server/schemas/doors.py +306 -0
  26. biostar_x_mcp_server/schemas/events.py +104 -0
  27. biostar_x_mcp_server/schemas/files.py +7 -0
  28. biostar_x_mcp_server/schemas/help.py +29 -0
  29. biostar_x_mcp_server/schemas/logs.py +33 -0
  30. biostar_x_mcp_server/schemas/occupancy.py +19 -0
  31. biostar_x_mcp_server/schemas/tool_response.py +29 -0
  32. biostar_x_mcp_server/schemas/users.py +166 -0
  33. biostar_x_mcp_server/server.py +335 -0
  34. biostar_x_mcp_server/session.py +221 -0
  35. biostar_x_mcp_server/tool_manager.py +172 -0
  36. biostar_x_mcp_server/tools/__init__.py +45 -0
  37. biostar_x_mcp_server/tools/access.py +510 -0
  38. biostar_x_mcp_server/tools/audit.py +227 -0
  39. biostar_x_mcp_server/tools/auth.py +59 -0
  40. biostar_x_mcp_server/tools/cards.py +269 -0
  41. biostar_x_mcp_server/tools/categories.py +197 -0
  42. biostar_x_mcp_server/tools/devices.py +1552 -0
  43. biostar_x_mcp_server/tools/doors.py +865 -0
  44. biostar_x_mcp_server/tools/events.py +305 -0
  45. biostar_x_mcp_server/tools/files.py +28 -0
  46. biostar_x_mcp_server/tools/help.py +80 -0
  47. biostar_x_mcp_server/tools/logs.py +123 -0
  48. biostar_x_mcp_server/tools/navigation.py +89 -0
  49. biostar_x_mcp_server/tools/occupancy.py +91 -0
  50. biostar_x_mcp_server/tools/users.py +1113 -0
  51. biostar_x_mcp_server/utils/__init__.py +31 -0
  52. biostar_x_mcp_server/utils/category_mapper.py +206 -0
  53. biostar_x_mcp_server/utils/decorators.py +101 -0
  54. biostar_x_mcp_server/utils/language_detector.py +51 -0
  55. biostar_x_mcp_server/utils/search.py +42 -0
  56. biostar_x_mcp_server/utils/timezone.py +122 -0
  57. suprema_biostar_mcp-1.0.1.dist-info/METADATA +163 -0
  58. suprema_biostar_mcp-1.0.1.dist-info/RECORD +61 -0
  59. suprema_biostar_mcp-1.0.1.dist-info/WHEEL +4 -0
  60. suprema_biostar_mcp-1.0.1.dist-info/entry_points.txt +2 -0
  61. suprema_biostar_mcp-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1051 @@
1
+ """
2
+ BioStar X 로그 분석 핸들러
3
+ """
4
+ import os
5
+ import logging
6
+ import glob
7
+ import psutil
8
+ import platform
9
+ from datetime import datetime, timedelta
10
+ from typing import List, Dict, Any, Optional
11
+ from mcp.types import TextContent
12
+ from .base_handler import BaseHandler
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LogHandler(BaseHandler):
18
+ """BioStar X 로그 분석을 위한 핸들러"""
19
+
20
+ def __init__(self, session):
21
+ super().__init__(session)
22
+ self.default_log_path = r"C:\Program Files\BioStar X\logs"
23
+
24
+ async def analyze_server_logs(
25
+ self,
26
+ log_path: Optional[str] = None,
27
+ lines_to_read: int = 50,
28
+ **kwargs # 추가 파라미터 무시
29
+ ) -> List[TextContent]:
30
+ """BioStar X 서버 로그를 종합 분석합니다."""
31
+ try:
32
+ if not log_path:
33
+ log_path = self.default_log_path
34
+
35
+ logger.info(f"Checking log directory: {log_path}")
36
+
37
+ # 로그 파일 경로 확인
38
+ if not os.path.exists(log_path):
39
+ logger.error(f"Log directory not found: {log_path}")
40
+ return self.error_response(
41
+ "로그 디렉토리를 찾을 수 없습니다",
42
+ {"path": log_path, "suggestion": "BioStar X가 설치되어 있는지 확인하세요"}
43
+ )
44
+
45
+ # 오늘 날짜 기준으로 로그 파일 찾기
46
+ today = datetime.now()
47
+ log_files = self._find_today_log_files(log_path, today)
48
+
49
+ logger.info(f"Found log files: {log_files}")
50
+
51
+ if not log_files:
52
+ logger.warning(f"No log files found for today: {today.strftime('%Y-%m-%d')}")
53
+ return self.error_response(
54
+ "오늘 날짜의 로그 파일을 찾을 수 없습니다",
55
+ {"path": log_path, "date": today.strftime("%Y-%m-%d")}
56
+ )
57
+
58
+ # 각 로그 파일 분석
59
+ analysis_results = {}
60
+ for log_type, file_path in log_files.items():
61
+ if os.path.exists(file_path):
62
+ analysis_results[log_type] = await self._analyze_log_file(file_path, lines_to_read)
63
+ else:
64
+ analysis_results[log_type] = {"error": f"파일을 찾을 수 없습니다: {file_path}"}
65
+
66
+ # 종합 분석 결과 생성
67
+ summary = self._generate_log_summary(analysis_results, today)
68
+
69
+ # 상세 분석에서 로그 라인은 제외하고 통계만 포함
70
+ simplified_analysis = {}
71
+ for log_type, result in analysis_results.items():
72
+ if "error" in result:
73
+ simplified_analysis[log_type] = result
74
+ else:
75
+ log_analysis = result.get("log_analysis", {})
76
+ simplified_analysis[log_type] = {
77
+ "file_info": result.get("file_info", {}),
78
+ "statistics": {
79
+ "total_lines": log_analysis.get("total_lines", 0),
80
+ "error_count": log_analysis.get("error_count", 0),
81
+ "warning_count": log_analysis.get("warning_count", 0),
82
+ "info_count": log_analysis.get("info_count", 0),
83
+ "server_status": log_analysis.get("server_status", "unknown")
84
+ },
85
+ "sample_errors": log_analysis.get("recent_errors", [])[:3], # 최대 3개만
86
+ "sample_warnings": log_analysis.get("recent_warnings", [])[:3] # 최대 3개만
87
+ }
88
+
89
+ return self.success_response(
90
+ {
91
+ "analysis_date": today.strftime("%Y-%m-%d %H:%M:%S"),
92
+ "log_directory": log_path,
93
+ "analyzed_files": log_files,
94
+ "summary": summary,
95
+ "detailed_analysis": simplified_analysis
96
+ },
97
+ "BioStar X 서버 로그 분석이 완료되었습니다"
98
+ )
99
+
100
+ except Exception as e:
101
+ logger.error(f"Error during log analysis: {e}", exc_info=True)
102
+ return self.error_response(
103
+ "로그 분석 중 오류가 발생했습니다",
104
+ {"error": str(e)}
105
+ )
106
+
107
+ async def get_log_file_info(self, log_path: Optional[str] = None) -> List[TextContent]:
108
+ """로그 디렉토리의 파일 정보를 조회합니다."""
109
+ try:
110
+ if not log_path:
111
+ log_path = self.default_log_path
112
+
113
+ if not os.path.exists(log_path):
114
+ return self.error_response(
115
+ "로그 디렉토리를 찾을 수 없습니다",
116
+ {"path": log_path}
117
+ )
118
+
119
+ # 로그 파일 목록 수집
120
+ log_files = []
121
+ for file_path in glob.glob(os.path.join(log_path, "*.log")):
122
+ try:
123
+ stat = os.stat(file_path)
124
+ log_files.append({
125
+ "filename": os.path.basename(file_path),
126
+ "full_path": file_path,
127
+ "size_bytes": stat.st_size,
128
+ "size_mb": round(stat.st_size / (1024 * 1024), 2),
129
+ "modified_time": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
130
+ "created_time": datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M:%S")
131
+ })
132
+ except Exception as e:
133
+ logger.warning(f"Failed to get file info: {file_path}, error: {e}")
134
+
135
+ # 파일 크기순으로 정렬
136
+ log_files.sort(key=lambda x: x["size_bytes"], reverse=True)
137
+
138
+ return self.success_response(
139
+ {
140
+ "log_directory": log_path,
141
+ "total_files": len(log_files),
142
+ "files": log_files
143
+ },
144
+ f"로그 디렉토리 정보 조회 완료 ({len(log_files)}개 파일)"
145
+ )
146
+
147
+ except Exception as e:
148
+ logger.error(f"Error getting log file info: {e}", exc_info=True)
149
+ return self.error_response(
150
+ "로그 파일 정보 조회 중 오류가 발생했습니다",
151
+ {"error": str(e)}
152
+ )
153
+
154
+ async def read_specific_log(
155
+ self,
156
+ log_file_path: str,
157
+ lines_to_read: int = 50,
158
+ from_end: bool = True
159
+ ) -> List[TextContent]:
160
+ """특정 로그 파일의 내용을 읽어 분석합니다."""
161
+ try:
162
+ if not os.path.exists(log_file_path):
163
+ return self.error_response(
164
+ "로그 파일을 찾을 수 없습니다",
165
+ {"path": log_file_path}
166
+ )
167
+
168
+ # 로그 파일 분석
169
+ analysis = await self._analyze_log_file(log_file_path, lines_to_read, from_end)
170
+
171
+ return self.success_response(
172
+ {
173
+ "file_path": log_file_path,
174
+ "lines_read": lines_to_read,
175
+ "from_end": from_end,
176
+ "analysis": analysis
177
+ },
178
+ f"로그 파일 분석 완료: {os.path.basename(log_file_path)}"
179
+ )
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error reading log file: {e}", exc_info=True)
183
+ return self.error_response(
184
+ "로그 파일 읽기 중 오류가 발생했습니다",
185
+ {"error": str(e)}
186
+ )
187
+
188
+ def _find_today_log_files(self, log_path: str, today: datetime) -> Dict[str, str]:
189
+ """오늘 날짜의 로그 파일들을 찾습니다."""
190
+ log_files = {}
191
+ date_str = today.strftime("%Y-%m-%d")
192
+
193
+ # 각 로그 파일 패턴 확인
194
+ patterns = {
195
+ "main_server": f"biostar_{date_str}.log",
196
+ "device": f"biostar_device_{date_str}.log",
197
+ "gateway": "serverGateway.log",
198
+ "api_server": "acs.log"
199
+ }
200
+
201
+ for log_type, pattern in patterns.items():
202
+ file_path = os.path.join(log_path, pattern)
203
+ if os.path.exists(file_path):
204
+ log_files[log_type] = file_path
205
+ else:
206
+ # 날짜가 다른 경우 최신 파일 찾기
207
+ if log_type in ["main_server", "device"]:
208
+ # 날짜 패턴이 있는 파일들 중 최신 것 찾기
209
+ if log_type == "main_server":
210
+ search_pattern = os.path.join(log_path, "biostar_*.log")
211
+ else:
212
+ search_pattern = os.path.join(log_path, "biostar_device_*.log")
213
+
214
+ matching_files = glob.glob(search_pattern)
215
+ if matching_files:
216
+ # 파일명에서 날짜 추출하여 최신 파일 선택
217
+ latest_file = max(matching_files, key=lambda f: self._extract_date_from_filename(f))
218
+ log_files[log_type] = latest_file
219
+ else:
220
+ # 고정 파일명의 경우 그대로 시도
221
+ log_files[log_type] = file_path
222
+
223
+ return log_files
224
+
225
+ def _extract_date_from_filename(self, filepath: str) -> str:
226
+ """파일명에서 날짜를 추출합니다."""
227
+ filename = os.path.basename(filepath)
228
+ # biostar_2025-01-15.log 형태에서 날짜 추출
229
+ import re
230
+ match = re.search(r'(\d{4}-\d{2}-\d{2})', filename)
231
+ return match.group(1) if match else "0000-00-00"
232
+
233
+ async def _analyze_log_file(self, file_path: str, lines_to_read: int, from_end: bool = True) -> Dict[str, Any]:
234
+ """개별 로그 파일을 분석합니다."""
235
+ try:
236
+ # 파일 크기 확인
237
+ file_size = os.path.getsize(file_path)
238
+
239
+ # 로그 라인 읽기
240
+ lines = self._read_log_lines(file_path, lines_to_read, from_end)
241
+
242
+ if not lines:
243
+ return {"error": "로그 파일이 비어있거나 읽을 수 없습니다"}
244
+
245
+ # 로그 분석
246
+ analysis = {
247
+ "file_info": {
248
+ "path": file_path,
249
+ "size_bytes": file_size,
250
+ "size_mb": round(file_size / (1024 * 1024), 2),
251
+ "lines_analyzed": len(lines)
252
+ },
253
+ "log_analysis": self._analyze_log_content(lines, file_path)
254
+ }
255
+
256
+ return analysis
257
+
258
+ except Exception as e:
259
+ logger.error(f"Error analyzing log file: {file_path}, {e}")
260
+ return {"error": f"Log file analysis failed: {str(e)}"}
261
+
262
+ def _read_log_lines(self, file_path: str, lines_to_read: int, from_end: bool = True) -> List[str]:
263
+ """로그 파일에서 지정된 수의 라인을 읽습니다."""
264
+ try:
265
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
266
+ if from_end:
267
+ # 파일 끝에서부터 읽기
268
+ lines = []
269
+ # 파일을 뒤에서부터 읽기 위해 seek 사용
270
+ f.seek(0, 2) # 파일 끝으로 이동
271
+ file_size = f.tell()
272
+
273
+ # 뒤에서부터 읽기
274
+ buffer_size = min(8192, file_size)
275
+ position = file_size
276
+ buffer = ""
277
+
278
+ while len(lines) < lines_to_read and position > 0:
279
+ # 읽을 위치 계산
280
+ read_size = min(buffer_size, position)
281
+ position -= read_size
282
+
283
+ # 해당 위치로 이동하여 읽기
284
+ f.seek(position)
285
+ chunk = f.read(read_size)
286
+ buffer = chunk + buffer
287
+
288
+ # 라인 단위로 분리
289
+ while '\n' in buffer and len(lines) < lines_to_read:
290
+ line, buffer = buffer.rsplit('\n', 1)
291
+ if line.strip():
292
+ lines.insert(0, line.strip())
293
+
294
+ # 마지막 남은 버퍼 처리
295
+ if buffer.strip() and len(lines) < lines_to_read:
296
+ lines.insert(0, buffer.strip())
297
+
298
+ else:
299
+ # 파일 시작에서부터 읽기
300
+ lines = []
301
+ for i, line in enumerate(f):
302
+ if i >= lines_to_read:
303
+ break
304
+ if line.strip():
305
+ lines.append(line.strip())
306
+
307
+ return lines
308
+
309
+ except Exception as e:
310
+ logger.error(f"Failed to read log file: {file_path}, {e}")
311
+ return []
312
+
313
+ def _analyze_log_content(self, lines: List[str], file_path: str) -> Dict[str, Any]:
314
+ """로그 내용을 분석합니다."""
315
+ analysis = {
316
+ "total_lines": len(lines),
317
+ "error_count": 0,
318
+ "warning_count": 0,
319
+ "info_count": 0,
320
+ "recent_errors": [],
321
+ "recent_warnings": [],
322
+ "key_events": [],
323
+ "server_status": "unknown"
324
+ }
325
+
326
+ # 멀티라인 오류 처리를 위한 버퍼
327
+ current_error = []
328
+ in_error_block = False
329
+
330
+ # 로그 레벨별 분석
331
+ for i, line in enumerate(lines):
332
+ line_lower = line.lower()
333
+
334
+ # 오류 블록 시작 감지
335
+ if '[error]' in line_lower or 'exception' in line_lower:
336
+ if current_error and in_error_block:
337
+ # 이전 오류 저장
338
+ error_text = '\n'.join(current_error)
339
+ parsed_error = self._parse_log_line(error_text, file_path)
340
+ if len(analysis["recent_errors"]) < 5:
341
+ analysis["recent_errors"].append(parsed_error)
342
+
343
+ # 새 오류 블록 시작
344
+ current_error = [line]
345
+ in_error_block = True
346
+ analysis["error_count"] += 1
347
+
348
+ elif in_error_block:
349
+ # 오류 블록 계속 (스택 트레이스 등)
350
+ if line.strip().startswith(('at ', 'Caused by:', 'Suppressed:', '*__checkpoint')):
351
+ current_error.append(line)
352
+ elif line.strip() == '':
353
+ # 빈 줄로 오류 블록 종료
354
+ error_text = '\n'.join(current_error)
355
+ parsed_error = self._parse_log_line(error_text, file_path)
356
+ if len(analysis["recent_errors"]) < 5:
357
+ analysis["recent_errors"].append(parsed_error)
358
+ current_error = []
359
+ in_error_block = False
360
+ else:
361
+ # 새로운 로그 라인 시작
362
+ error_text = '\n'.join(current_error)
363
+ parsed_error = self._parse_log_line(error_text, file_path)
364
+ if len(analysis["recent_errors"]) < 5:
365
+ analysis["recent_errors"].append(parsed_error)
366
+ current_error = []
367
+ in_error_block = False
368
+
369
+ # 현재 라인 처리
370
+ if '[warn]' in line_lower or 'warning' in line_lower:
371
+ analysis["warning_count"] += 1
372
+ parsed_warning = self._parse_log_line(line, file_path)
373
+ if len(analysis["recent_warnings"]) < 5:
374
+ analysis["recent_warnings"].append(parsed_warning)
375
+ elif any(keyword in line_lower for keyword in ['info', 'started', 'connected']):
376
+ analysis["info_count"] += 1
377
+
378
+ elif '[warn]' in line_lower or 'warning' in line_lower:
379
+ analysis["warning_count"] += 1
380
+ parsed_warning = self._parse_log_line(line, file_path)
381
+ if len(analysis["recent_warnings"]) < 5:
382
+ analysis["recent_warnings"].append(parsed_warning)
383
+
384
+ elif any(keyword in line_lower for keyword in ['info', 'started', 'connected']):
385
+ analysis["info_count"] += 1
386
+
387
+ # 주요 이벤트 추출
388
+ if any(keyword in line_lower for keyword in [
389
+ 'started', 'stopped', 'connected', 'disconnected',
390
+ 'login', 'logout', 'access', 'door', 'device'
391
+ ]):
392
+ if len(analysis["key_events"]) < 10:
393
+ parsed_event = self._parse_log_line(line, file_path)
394
+ analysis["key_events"].append(parsed_event)
395
+
396
+ # 마지막 오류 블록 처리
397
+ if current_error and in_error_block:
398
+ error_text = '\n'.join(current_error)
399
+ parsed_error = self._parse_log_line(error_text, file_path)
400
+ if len(analysis["recent_errors"]) < 5:
401
+ analysis["recent_errors"].append(parsed_error)
402
+
403
+ # 서버 상태 판단
404
+ if analysis["error_count"] == 0 and analysis["warning_count"] < 5:
405
+ analysis["server_status"] = "healthy"
406
+ elif analysis["error_count"] < 5:
407
+ analysis["server_status"] = "warning"
408
+ else:
409
+ analysis["server_status"] = "error"
410
+
411
+ return analysis
412
+
413
+ def _parse_log_line(self, log_text: str, file_path: str) -> Dict[str, Any]:
414
+ """로그 라인을 파싱하여 구조화된 정보를 추출합니다."""
415
+ import re
416
+
417
+ parsed = {
418
+ "raw_message": log_text[:500] if len(log_text) > 500 else log_text,
419
+ "timestamp": None,
420
+ "level": None,
421
+ "category": None,
422
+ "thread": None,
423
+ "source_file": None,
424
+ "function": None,
425
+ "message": None,
426
+ "error_type": None,
427
+ "device_id": None,
428
+ "task_name": None,
429
+ "error_code": None,
430
+ "location": None,
431
+ "port": None,
432
+ "endpoint": None,
433
+ "keywords": []
434
+ }
435
+
436
+ first_line = log_text.split('\n')[0]
437
+
438
+ # BioStar X 로그 형식: 25/10/29 12:50:20.319445 I <SYS> [0x00005d94] [DevicePacketParser.c:3954(...)]
439
+ biostar_match = re.match(r'(\d{2}/\d{2}/\d{2}\s+[\d:.]+)\s+([IWE])\s+<(\w+)>\s+\[([^\]]+)\]\s+\[([^\]]+)\](?:\s+-\s+(.+))?', first_line)
440
+
441
+ if biostar_match:
442
+ # BioStar X 형식
443
+ parsed["timestamp"] = biostar_match.group(1)
444
+ level_code = biostar_match.group(2)
445
+ parsed["level"] = {"I": "INFO", "W": "WARN", "E": "ERROR"}.get(level_code, level_code)
446
+ parsed["category"] = biostar_match.group(3)
447
+ parsed["thread"] = biostar_match.group(4)
448
+
449
+ # 소스 파일과 함수 파싱: DevicePacketParser.c:3954(DevicePacketParser::parseDevic)
450
+ source_info = biostar_match.group(5)
451
+ source_match = re.match(r'([^:]+):(\d+)\(([^)]+)\)', source_info)
452
+ if source_match:
453
+ parsed["source_file"] = f"{source_match.group(1)}:{source_match.group(2)}"
454
+ parsed["function"] = source_match.group(3)
455
+
456
+ # 메시지
457
+ if biostar_match.group(6):
458
+ parsed["message"] = biostar_match.group(6).strip()
459
+ else:
460
+ # Spring Gateway 형식: [ERROR] 10-29 10:54:38.921] [reactor-http-nio-2]
461
+ gateway_match = re.search(r'\[(?:ERROR|WARN|INFO)\]\s+([\d-]+\s+[\d:.]+)\]\s+\[([^\]]+)\]\s+-\s+([^:]+):(\d+)\s+:\w+\s+-\s+(.+)', first_line)
462
+ if gateway_match:
463
+ parsed["timestamp"] = gateway_match.group(1)
464
+ parsed["thread"] = gateway_match.group(2)
465
+ parsed["source_file"] = f"{gateway_match.group(3)}:{gateway_match.group(4)}"
466
+ parsed["message"] = gateway_match.group(5).strip()
467
+
468
+ level_match = re.search(r'\[(ERROR|WARN|INFO)\]', first_line)
469
+ if level_match:
470
+ parsed["level"] = level_match.group(1)
471
+
472
+ # 장치 ID 추출: device=547840779, id:547840779, DEVID IN (547840779)
473
+ device_patterns = [
474
+ r'device[=:](\d+)',
475
+ r'id[=:](\d+)',
476
+ r'DEVID\s+IN\s+\((\d+)\)'
477
+ ]
478
+ for pattern in device_patterns:
479
+ device_match = re.search(pattern, log_text)
480
+ if device_match:
481
+ parsed["device_id"] = device_match.group(1)
482
+ break
483
+
484
+ # 태스크 이름 추출: TASK_GET_CONFIG, TASK_RESTORE_CONNECTION
485
+ task_match = re.search(r'TASK_([A-Z_]+)', log_text)
486
+ if task_match:
487
+ parsed["task_name"] = f"TASK_{task_match.group(1)}"
488
+
489
+ # 오류 코드 추출: error code: 2012
490
+ error_code_match = re.search(r'error\s+code:\s*(\d+)', log_text, re.IGNORECASE)
491
+ if error_code_match:
492
+ parsed["error_code"] = error_code_match.group(1)
493
+
494
+ # 오류 유형 분석
495
+ error_types = {
496
+ 'Connection refused': 'Connection refused',
497
+ 'ConnectException': 'Connection Exception',
498
+ 'WebSocket': 'WebSocket error',
499
+ 'License': 'License error',
500
+ 'Database': 'Database error',
501
+ 'ZooKeeper': 'ZooKeeper error',
502
+ 'allocate.*failed': 'Memory allocation error',
503
+ 'no devcaps': 'Device capability error',
504
+ 'Couldn\'t save': 'Save operation failed',
505
+ 'request not found': 'Request not found',
506
+ 'Cannot execute': 'Command execution failed',
507
+ 'Failed to initialize': 'Initialization failed',
508
+ 'tcp failed': 'TCP connection failed'
509
+ }
510
+
511
+ for pattern, error_type in error_types.items():
512
+ if re.search(pattern, log_text, re.IGNORECASE):
513
+ parsed["error_type"] = error_type
514
+ break
515
+
516
+ # IP:Port 추출
517
+ location_match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', log_text)
518
+ if location_match:
519
+ parsed["location"] = location_match.group(1)
520
+ parsed["port"] = location_match.group(2)
521
+
522
+ # HTTP 엔드포인트 추출
523
+ endpoint_match = re.search(r'HTTP\s+\w+\s+"([^"]+)"', log_text)
524
+ if endpoint_match:
525
+ parsed["endpoint"] = endpoint_match.group(1)
526
+
527
+ # 키워드 추출
528
+ keywords = []
529
+ keyword_patterns = [
530
+ 'Connection refused', 'getsockopt', 'WebSocket', 'session',
531
+ 'License', 'validation', 'failed', 'expired', 'timeout',
532
+ 'authentication', 'login', 'device', 'door', 'access',
533
+ 'database', 'memory', 'exception', 'error', 'ZooKeeper',
534
+ 'allocate', 'devcaps', 'tcp', 'upload', 'monitoring',
535
+ 'TASK_', 'retry', 'initialize', 'execute'
536
+ ]
537
+ for keyword in keyword_patterns:
538
+ if keyword.lower() in log_text.lower():
539
+ if keyword == 'TASK_' and parsed["task_name"]:
540
+ keywords.append(parsed["task_name"])
541
+ elif keyword != 'TASK_':
542
+ keywords.append(keyword)
543
+
544
+ # 중복 제거
545
+ parsed["keywords"] = list(set(keywords))
546
+
547
+ return parsed
548
+
549
+ def _generate_log_summary(self, analysis_results: Dict[str, Any], today: datetime) -> Dict[str, Any]:
550
+ """로그 분석 결과를 종합하여 요약을 생성합니다."""
551
+ summary = {
552
+ "analysis_timestamp": today.strftime("%Y-%m-%d %H:%M:%S"),
553
+ "overall_status": "unknown",
554
+ "total_errors": 0,
555
+ "total_warnings": 0,
556
+ "server_components": {},
557
+ "critical_issues": [],
558
+ "error_details": [],
559
+ "warning_details": [],
560
+ "recommendations": []
561
+ }
562
+
563
+ # 각 컴포넌트별 상태 분석
564
+ for component, result in analysis_results.items():
565
+ if "error" in result:
566
+ summary["server_components"][component] = {
567
+ "status": "error",
568
+ "message": result["error"]
569
+ }
570
+ summary["critical_issues"].append(f"{component}: {result['error']}")
571
+ else:
572
+ log_analysis = result.get("log_analysis", {})
573
+ status = log_analysis.get("server_status", "unknown")
574
+ error_count = log_analysis.get("error_count", 0)
575
+ warning_count = log_analysis.get("warning_count", 0)
576
+ recent_errors = log_analysis.get("recent_errors", [])
577
+ recent_warnings = log_analysis.get("recent_warnings", [])
578
+
579
+ summary["server_components"][component] = {
580
+ "status": status,
581
+ "errors": error_count,
582
+ "warnings": warning_count,
583
+ "recent_errors": recent_errors,
584
+ "recent_warnings": recent_warnings
585
+ }
586
+
587
+ summary["total_errors"] += error_count
588
+ summary["total_warnings"] += warning_count
589
+
590
+ # 실제 오류 메시지를 error_details에 추가
591
+ for error_data in recent_errors:
592
+ # error_data는 이제 딕셔너리 (parsed log)
593
+ if isinstance(error_data, dict):
594
+ error_msg = error_data.get("raw_message", "")
595
+ summary["error_details"].append({
596
+ "component": component,
597
+ "parsed_data": error_data,
598
+ "analysis": self._analyze_error_message(error_msg)
599
+ })
600
+ else:
601
+ # 이전 형식 호환성
602
+ summary["error_details"].append({
603
+ "component": component,
604
+ "message": error_data,
605
+ "analysis": self._analyze_error_message(error_data)
606
+ })
607
+
608
+ # 실제 경고 메시지를 warning_details에 추가
609
+ for warning_data in recent_warnings:
610
+ if isinstance(warning_data, dict):
611
+ summary["warning_details"].append({
612
+ "component": component,
613
+ "parsed_data": warning_data
614
+ })
615
+ else:
616
+ # 이전 형식 호환성
617
+ summary["warning_details"].append({
618
+ "component": component,
619
+ "message": warning_data
620
+ })
621
+
622
+ # 심각한 오류가 있는 경우
623
+ if error_count > 10:
624
+ summary["critical_issues"].append(f"{component}: {error_count}개의 오류 발생")
625
+
626
+ # 전체 상태 판단
627
+ if summary["total_errors"] == 0 and summary["total_warnings"] < 10:
628
+ summary["overall_status"] = "healthy"
629
+ elif summary["total_errors"] < 5:
630
+ summary["overall_status"] = "warning"
631
+ else:
632
+ summary["overall_status"] = "critical"
633
+
634
+ # 권장사항 생성
635
+ if summary["overall_status"] == "critical":
636
+ summary["recommendations"].append("서버 상태가 심각합니다. 즉시 점검이 필요합니다.")
637
+ elif summary["overall_status"] == "warning":
638
+ summary["recommendations"].append("일부 경고가 있습니다. 모니터링을 강화하세요.")
639
+ else:
640
+ summary["recommendations"].append("서버 상태가 양호합니다.")
641
+
642
+ if summary["total_errors"] > 0:
643
+ summary["recommendations"].append("오류 로그를 상세히 검토하여 원인을 파악하세요.")
644
+
645
+ return summary
646
+
647
+ def _analyze_error_message(self, error_msg: str) -> Dict[str, str]:
648
+ """오류 메시지를 분석하여 원인과 해결 방법을 제안합니다."""
649
+ error_lower = error_msg.lower()
650
+
651
+ # 일반적인 오류 패턴 분석
652
+ if "license" in error_lower or "라이센스" in error_lower:
653
+ return {
654
+ "type": "라이센스 오류",
655
+ "cause": "BioStar X 라이센스가 만료되었거나 유효하지 않습니다",
656
+ "solution": "1. 라이센스 파일 확인\n2. 라이센스 갱신 필요\n3. 라이센스 서버 연결 상태 확인"
657
+ }
658
+ elif "connection" in error_lower or "연결" in error_lower:
659
+ return {
660
+ "type": "연결 오류",
661
+ "cause": "네트워크 연결 또는 서비스 간 통신 문제",
662
+ "solution": "1. 네트워크 연결 상태 확인\n2. 방화벽 설정 확인\n3. 서비스 재시작 시도"
663
+ }
664
+ elif "database" in error_lower or "db" in error_lower or "데이터베이스" in error_lower:
665
+ return {
666
+ "type": "데이터베이스 오류",
667
+ "cause": "데이터베이스 연결 또는 쿼리 실행 실패",
668
+ "solution": "1. 데이터베이스 서비스 상태 확인\n2. 연결 문자열 확인\n3. 데이터베이스 로그 확인"
669
+ }
670
+ elif "websocket" in error_lower:
671
+ return {
672
+ "type": "WebSocket 오류",
673
+ "cause": "실시간 통신 연결 문제",
674
+ "solution": "1. 클라이언트 재연결 시도\n2. 서버 재시작 고려\n3. 네트워크 안정성 확인"
675
+ }
676
+ elif "memory" in error_lower or "메모리" in error_lower:
677
+ return {
678
+ "type": "메모리 오류",
679
+ "cause": "시스템 메모리 부족 또는 메모리 누수",
680
+ "solution": "1. 시스템 메모리 사용량 확인\n2. 불필요한 프로세스 종료\n3. 서버 재시작 고려"
681
+ }
682
+ elif "timeout" in error_lower or "시간초과" in error_lower:
683
+ return {
684
+ "type": "시간 초과 오류",
685
+ "cause": "요청 처리 시간이 너무 오래 걸림",
686
+ "solution": "1. 네트워크 지연 확인\n2. 서버 성능 확인\n3. 타임아웃 설정 조정 고려"
687
+ }
688
+ elif "authentication" in error_lower or "인증" in error_lower or "login" in error_lower:
689
+ return {
690
+ "type": "인증 오류",
691
+ "cause": "사용자 인증 실패 또는 세션 만료",
692
+ "solution": "1. 사용자 계정 확인\n2. 비밀번호 재설정\n3. 세션 타임아웃 설정 확인"
693
+ }
694
+ elif "device" in error_lower or "장치" in error_lower:
695
+ return {
696
+ "type": "장치 오류",
697
+ "cause": "출입 제어 장치와의 통신 문제",
698
+ "solution": "1. 장치 연결 상태 확인\n2. 장치 전원 확인\n3. 네트워크 케이블 확인"
699
+ }
700
+ elif "service" in error_lower or "서비스" in error_lower:
701
+ return {
702
+ "type": "서비스 오류",
703
+ "cause": "BioStar 서비스 시작 또는 실행 실패",
704
+ "solution": "1. 서비스 상태 확인\n2. 서비스 재시작\n3. 이벤트 로그 확인"
705
+ }
706
+ elif "configuration" in error_lower or "config" in error_lower or "설정" in error_lower:
707
+ return {
708
+ "type": "설정 오류",
709
+ "cause": "잘못된 설정 또는 설정 파일 손상",
710
+ "solution": "1. 설정 파일 확인\n2. 기본 설정으로 복원\n3. 설정 백업 복구"
711
+ }
712
+ else:
713
+ return {
714
+ "type": "일반 오류",
715
+ "cause": "상세 분석 필요",
716
+ "solution": "1. 전체 로그 메시지 확인\n2. 관련 문서 참조\n3. 기술 지원팀 문의"
717
+ }
718
+
719
+ async def get_system_resources(self, include_all_processes: bool = False) -> List[TextContent]:
720
+ """시스템 리소스 사용량을 분석합니다."""
721
+ try:
722
+ # 시스템 정보 수집
723
+ system_info = self._get_system_info()
724
+
725
+ # BioStar 관련 프로세스 찾기
726
+ biostar_processes = self._find_biostar_processes()
727
+
728
+ # 전체 시스템 리소스 정보
729
+ system_resources = self._get_system_resources()
730
+
731
+ # 프로세스 정보 (BioStar 관련 또는 전체)
732
+ if include_all_processes:
733
+ all_processes = self._get_all_processes_info()
734
+ process_info = {
735
+ "biostar_processes": biostar_processes,
736
+ "all_processes": all_processes
737
+ }
738
+ else:
739
+ process_info = {
740
+ "biostar_processes": biostar_processes
741
+ }
742
+
743
+ # 종합 분석
744
+ analysis = self._analyze_system_status(system_info, biostar_processes, system_resources)
745
+
746
+ return self.success_response(
747
+ {
748
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
749
+ "system_info": system_info,
750
+ "system_resources": system_resources,
751
+ "process_info": process_info,
752
+ "analysis": analysis
753
+ },
754
+ "시스템 리소스 분석이 완료되었습니다"
755
+ )
756
+
757
+ except Exception as e:
758
+ logger.error(f"Error analyzing system resources: {e}", exc_info=True)
759
+ return self.error_response(
760
+ "시스템 리소스 분석 중 오류가 발생했습니다",
761
+ {"error": str(e)}
762
+ )
763
+
764
+ async def analyze_server_status(
765
+ self,
766
+ log_path: Optional[str] = None,
767
+ lines_to_read: int = 50,
768
+ include_system_resources: bool = True
769
+ ) -> List[TextContent]:
770
+ """BioStar X 서버의 종합적인 상태를 분석합니다."""
771
+ try:
772
+ # 로그 분석
773
+ log_analysis = await self.analyze_server_logs(log_path, lines_to_read)
774
+
775
+ # 시스템 리소스 분석
776
+ system_analysis = None
777
+ if include_system_resources:
778
+ system_analysis = await self.get_system_resources(include_all_processes=False)
779
+
780
+ # 종합 분석 결과 생성
781
+ combined_analysis = self._combine_analysis_results(log_analysis, system_analysis)
782
+
783
+ return self.success_response(
784
+ combined_analysis,
785
+ "BioStar X 서버 종합 상태 분석이 완료되었습니다"
786
+ )
787
+
788
+ except Exception as e:
789
+ logger.error(f"Error analyzing server status: {e}", exc_info=True)
790
+ return self.error_response(
791
+ "서버 상태 분석 중 오류가 발생했습니다",
792
+ {"error": str(e)}
793
+ )
794
+
795
+ def _get_system_info(self) -> Dict[str, Any]:
796
+ """시스템 기본 정보를 수집합니다."""
797
+ try:
798
+ return {
799
+ "platform": platform.platform(),
800
+ "system": platform.system(),
801
+ "release": platform.release(),
802
+ "version": platform.version(),
803
+ "machine": platform.machine(),
804
+ "processor": platform.processor(),
805
+ "hostname": platform.node(),
806
+ "boot_time": datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")
807
+ }
808
+ except Exception as e:
809
+ logger.error(f"Failed to collect system info: {e}")
810
+ return {"error": str(e)}
811
+
812
+ def _find_biostar_processes(self) -> List[Dict[str, Any]]:
813
+ """BioStar 관련 프로세스를 찾습니다."""
814
+ biostar_processes = []
815
+
816
+ try:
817
+ for proc in psutil.process_iter(['pid', 'name', 'memory_info', 'cpu_percent', 'status', 'create_time']):
818
+ try:
819
+ proc_info = proc.info
820
+ proc_name = proc_info['name'].lower()
821
+
822
+ # BioStar 관련 프로세스 필터링
823
+ if any(keyword in proc_name for keyword in ['biostar', 'bio', 'star']):
824
+ memory_mb = round(proc_info['memory_info'].rss / (1024 * 1024), 2)
825
+ create_time = datetime.fromtimestamp(proc_info['create_time']).strftime("%Y-%m-%d %H:%M:%S")
826
+
827
+ biostar_processes.append({
828
+ "pid": proc_info['pid'],
829
+ "name": proc_info['name'],
830
+ "memory_mb": memory_mb,
831
+ "cpu_percent": proc_info['cpu_percent'],
832
+ "status": proc_info['status'],
833
+ "create_time": create_time,
834
+ "memory_percent": round(proc.memory_percent(), 2)
835
+ })
836
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
837
+ continue
838
+
839
+ except Exception as e:
840
+ logger.error(f"Failed to search BioStar processes: {e}")
841
+
842
+ return biostar_processes
843
+
844
+ def _get_system_resources(self) -> Dict[str, Any]:
845
+ """시스템 리소스 사용량을 수집합니다."""
846
+ try:
847
+ # CPU 정보
848
+ cpu_percent = psutil.cpu_percent(interval=1)
849
+ cpu_count = psutil.cpu_count()
850
+ cpu_freq = psutil.cpu_freq()
851
+
852
+ # 메모리 정보
853
+ memory = psutil.virtual_memory()
854
+ swap = psutil.swap_memory()
855
+
856
+ # 디스크 정보
857
+ disk = psutil.disk_usage('/')
858
+
859
+ # 네트워크 정보
860
+ network = psutil.net_io_counters()
861
+
862
+ return {
863
+ "cpu": {
864
+ "usage_percent": cpu_percent,
865
+ "count": cpu_count,
866
+ "frequency_mhz": round(cpu_freq.current, 2) if cpu_freq else None,
867
+ "max_frequency_mhz": round(cpu_freq.max, 2) if cpu_freq else None
868
+ },
869
+ "memory": {
870
+ "total_gb": round(memory.total / (1024**3), 2),
871
+ "available_gb": round(memory.available / (1024**3), 2),
872
+ "used_gb": round(memory.used / (1024**3), 2),
873
+ "usage_percent": memory.percent,
874
+ "free_gb": round(memory.free / (1024**3), 2)
875
+ },
876
+ "swap": {
877
+ "total_gb": round(swap.total / (1024**3), 2),
878
+ "used_gb": round(swap.used / (1024**3), 2),
879
+ "free_gb": round(swap.free / (1024**3), 2),
880
+ "usage_percent": swap.percent
881
+ },
882
+ "disk": {
883
+ "total_gb": round(disk.total / (1024**3), 2),
884
+ "used_gb": round(disk.used / (1024**3), 2),
885
+ "free_gb": round(disk.free / (1024**3), 2),
886
+ "usage_percent": round((disk.used / disk.total) * 100, 2)
887
+ },
888
+ "network": {
889
+ "bytes_sent_mb": round(network.bytes_sent / (1024**2), 2),
890
+ "bytes_recv_mb": round(network.bytes_recv / (1024**2), 2),
891
+ "packets_sent": network.packets_sent,
892
+ "packets_recv": network.packets_recv
893
+ }
894
+ }
895
+ except Exception as e:
896
+ logger.error(f"Failed to collect system resources: {e}")
897
+ return {"error": str(e)}
898
+
899
+ def _get_all_processes_info(self) -> List[Dict[str, Any]]:
900
+ """모든 프로세스 정보를 수집합니다 (메모리 사용량 상위 20개)."""
901
+ processes = []
902
+
903
+ try:
904
+ for proc in psutil.process_iter(['pid', 'name', 'memory_info', 'cpu_percent', 'status']):
905
+ try:
906
+ proc_info = proc.info
907
+ memory_mb = round(proc_info['memory_info'].rss / (1024 * 1024), 2)
908
+
909
+ processes.append({
910
+ "pid": proc_info['pid'],
911
+ "name": proc_info['name'],
912
+ "memory_mb": memory_mb,
913
+ "cpu_percent": proc_info['cpu_percent'],
914
+ "status": proc_info['status']
915
+ })
916
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
917
+ continue
918
+
919
+ # 메모리 사용량 순으로 정렬하고 상위 20개만 반환
920
+ processes.sort(key=lambda x: x['memory_mb'], reverse=True)
921
+ return processes[:20]
922
+
923
+ except Exception as e:
924
+ logger.error(f"Failed to collect all process info: {e}")
925
+ return []
926
+
927
+ def _analyze_system_status(
928
+ self,
929
+ system_info: Dict[str, Any],
930
+ biostar_processes: List[Dict[str, Any]],
931
+ system_resources: Dict[str, Any]
932
+ ) -> Dict[str, Any]:
933
+ """시스템 상태를 종합 분석합니다."""
934
+ analysis = {
935
+ "overall_status": "unknown",
936
+ "biostar_status": "unknown",
937
+ "resource_status": "unknown",
938
+ "recommendations": [],
939
+ "alerts": []
940
+ }
941
+
942
+ try:
943
+ # BioStar 프로세스 상태 분석
944
+ if not biostar_processes:
945
+ analysis["biostar_status"] = "not_running"
946
+ analysis["alerts"].append("BioStar 관련 프로세스가 실행되지 않고 있습니다")
947
+ else:
948
+ total_biostar_memory = sum(proc['memory_mb'] for proc in biostar_processes)
949
+ if total_biostar_memory > 1000: # 1GB 이상
950
+ analysis["biostar_status"] = "high_memory_usage"
951
+ analysis["alerts"].append(f"BioStar 프로세스들의 메모리 사용량이 높습니다 ({total_biostar_memory:.1f}MB)")
952
+ elif total_biostar_memory < 50: # 50MB 미만
953
+ analysis["biostar_status"] = "low_memory_usage"
954
+ analysis["recommendations"].append("BioStar 프로세스들의 메모리 사용량이 낮습니다. 정상 작동 확인 필요")
955
+ else:
956
+ analysis["biostar_status"] = "normal"
957
+
958
+ # 시스템 리소스 상태 분석
959
+ if "error" not in system_resources:
960
+ memory_usage = system_resources.get("memory", {}).get("usage_percent", 0)
961
+ cpu_usage = system_resources.get("cpu", {}).get("usage_percent", 0)
962
+ disk_usage = system_resources.get("disk", {}).get("usage_percent", 0)
963
+
964
+ if memory_usage > 90:
965
+ analysis["resource_status"] = "critical"
966
+ analysis["alerts"].append(f"메모리 사용량이 매우 높습니다 ({memory_usage:.1f}%)")
967
+ elif memory_usage > 80:
968
+ analysis["resource_status"] = "warning"
969
+ analysis["alerts"].append(f"메모리 사용량이 높습니다 ({memory_usage:.1f}%)")
970
+ elif cpu_usage > 90:
971
+ analysis["resource_status"] = "critical"
972
+ analysis["alerts"].append(f"CPU 사용량이 매우 높습니다 ({cpu_usage:.1f}%)")
973
+ elif disk_usage > 90:
974
+ analysis["resource_status"] = "critical"
975
+ analysis["alerts"].append(f"디스크 사용량이 매우 높습니다 ({disk_usage:.1f}%)")
976
+ else:
977
+ analysis["resource_status"] = "normal"
978
+
979
+ # 전체 상태 판단
980
+ if analysis["biostar_status"] == "not_running" or analysis["resource_status"] == "critical":
981
+ analysis["overall_status"] = "critical"
982
+ elif analysis["biostar_status"] in ["high_memory_usage", "low_memory_usage"] or analysis["resource_status"] == "warning":
983
+ analysis["overall_status"] = "warning"
984
+ else:
985
+ analysis["overall_status"] = "healthy"
986
+
987
+ # 권장사항 추가
988
+ if analysis["overall_status"] == "healthy":
989
+ analysis["recommendations"].append("시스템 상태가 양호합니다. 정기적인 모니터링을 계속하세요.")
990
+
991
+ except Exception as e:
992
+ logger.error(f"Failed to analyze system status: {e}")
993
+ analysis["error"] = str(e)
994
+
995
+ return analysis
996
+
997
+ def _combine_analysis_results(
998
+ self,
999
+ log_analysis: List[TextContent],
1000
+ system_analysis: Optional[List[TextContent]]
1001
+ ) -> Dict[str, Any]:
1002
+ """로그 분석과 시스템 분석 결과를 결합합니다."""
1003
+ combined = {
1004
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1005
+ "log_analysis": None,
1006
+ "system_analysis": None,
1007
+ "combined_status": "unknown",
1008
+ "summary": {}
1009
+ }
1010
+
1011
+ try:
1012
+ # 로그 분석 결과 추출
1013
+ if log_analysis and len(log_analysis) > 0:
1014
+ log_content = log_analysis[0].text
1015
+ import json
1016
+ log_data = json.loads(log_content)
1017
+ combined["log_analysis"] = log_data.get("data", {})
1018
+
1019
+ # 시스템 분석 결과 추출
1020
+ if system_analysis and len(system_analysis) > 0:
1021
+ system_content = system_analysis[0].text
1022
+ import json
1023
+ system_data = json.loads(system_content)
1024
+ combined["system_analysis"] = system_data.get("data", {})
1025
+
1026
+ # 종합 상태 판단
1027
+ log_status = combined["log_analysis"].get("summary", {}).get("overall_status", "unknown") if combined["log_analysis"] else "unknown"
1028
+ system_status = combined["system_analysis"].get("analysis", {}).get("overall_status", "unknown") if combined["system_analysis"] else "unknown"
1029
+
1030
+ if log_status == "critical" or system_status == "critical":
1031
+ combined["combined_status"] = "critical"
1032
+ elif log_status == "warning" or system_status == "warning":
1033
+ combined["combined_status"] = "warning"
1034
+ elif log_status == "healthy" and system_status == "healthy":
1035
+ combined["combined_status"] = "healthy"
1036
+ else:
1037
+ combined["combined_status"] = "unknown"
1038
+
1039
+ # 요약 정보 생성
1040
+ combined["summary"] = {
1041
+ "log_status": log_status,
1042
+ "system_status": system_status,
1043
+ "combined_status": combined["combined_status"],
1044
+ "biostar_processes_count": len(combined["system_analysis"].get("process_info", {}).get("biostar_processes", [])) if combined["system_analysis"] else 0
1045
+ }
1046
+
1047
+ except Exception as e:
1048
+ logger.error(f"Failed to combine analysis results: {e}")
1049
+ combined["error"] = str(e)
1050
+
1051
+ return combined