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.
- biostar_x_mcp_server/__init__.py +25 -0
- biostar_x_mcp_server/__main__.py +15 -0
- biostar_x_mcp_server/config.py +87 -0
- biostar_x_mcp_server/handlers/__init__.py +35 -0
- biostar_x_mcp_server/handlers/access_handler.py +2162 -0
- biostar_x_mcp_server/handlers/audit_handler.py +489 -0
- biostar_x_mcp_server/handlers/auth_handler.py +216 -0
- biostar_x_mcp_server/handlers/base_handler.py +228 -0
- biostar_x_mcp_server/handlers/card_handler.py +746 -0
- biostar_x_mcp_server/handlers/device_handler.py +4344 -0
- biostar_x_mcp_server/handlers/door_handler.py +3969 -0
- biostar_x_mcp_server/handlers/event_handler.py +1331 -0
- biostar_x_mcp_server/handlers/file_handler.py +212 -0
- biostar_x_mcp_server/handlers/help_web_handler.py +379 -0
- biostar_x_mcp_server/handlers/log_handler.py +1051 -0
- biostar_x_mcp_server/handlers/navigation_handler.py +109 -0
- biostar_x_mcp_server/handlers/occupancy_handler.py +541 -0
- biostar_x_mcp_server/handlers/user_handler.py +3568 -0
- biostar_x_mcp_server/schemas/__init__.py +21 -0
- biostar_x_mcp_server/schemas/access.py +158 -0
- biostar_x_mcp_server/schemas/audit.py +73 -0
- biostar_x_mcp_server/schemas/auth.py +24 -0
- biostar_x_mcp_server/schemas/cards.py +128 -0
- biostar_x_mcp_server/schemas/devices.py +496 -0
- biostar_x_mcp_server/schemas/doors.py +306 -0
- biostar_x_mcp_server/schemas/events.py +104 -0
- biostar_x_mcp_server/schemas/files.py +7 -0
- biostar_x_mcp_server/schemas/help.py +29 -0
- biostar_x_mcp_server/schemas/logs.py +33 -0
- biostar_x_mcp_server/schemas/occupancy.py +19 -0
- biostar_x_mcp_server/schemas/tool_response.py +29 -0
- biostar_x_mcp_server/schemas/users.py +166 -0
- biostar_x_mcp_server/server.py +335 -0
- biostar_x_mcp_server/session.py +221 -0
- biostar_x_mcp_server/tool_manager.py +172 -0
- biostar_x_mcp_server/tools/__init__.py +45 -0
- biostar_x_mcp_server/tools/access.py +510 -0
- biostar_x_mcp_server/tools/audit.py +227 -0
- biostar_x_mcp_server/tools/auth.py +59 -0
- biostar_x_mcp_server/tools/cards.py +269 -0
- biostar_x_mcp_server/tools/categories.py +197 -0
- biostar_x_mcp_server/tools/devices.py +1552 -0
- biostar_x_mcp_server/tools/doors.py +865 -0
- biostar_x_mcp_server/tools/events.py +305 -0
- biostar_x_mcp_server/tools/files.py +28 -0
- biostar_x_mcp_server/tools/help.py +80 -0
- biostar_x_mcp_server/tools/logs.py +123 -0
- biostar_x_mcp_server/tools/navigation.py +89 -0
- biostar_x_mcp_server/tools/occupancy.py +91 -0
- biostar_x_mcp_server/tools/users.py +1113 -0
- biostar_x_mcp_server/utils/__init__.py +31 -0
- biostar_x_mcp_server/utils/category_mapper.py +206 -0
- biostar_x_mcp_server/utils/decorators.py +101 -0
- biostar_x_mcp_server/utils/language_detector.py +51 -0
- biostar_x_mcp_server/utils/search.py +42 -0
- biostar_x_mcp_server/utils/timezone.py +122 -0
- suprema_biostar_mcp-1.0.1.dist-info/METADATA +163 -0
- suprema_biostar_mcp-1.0.1.dist-info/RECORD +61 -0
- suprema_biostar_mcp-1.0.1.dist-info/WHEEL +4 -0
- suprema_biostar_mcp-1.0.1.dist-info/entry_points.txt +2 -0
- 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
|