Commit
·
a590b71
1
Parent(s):
904689f
Cleanup: Remove old rule-based agent files, all agents now use DeepSeek via smolagents
Browse files- agents/__pycache__/__init__.cpython-314.pyc +0 -0
- agents/anomaly_agent.py +0 -415
- agents/parser_agent.py +0 -217
- agents/rca_agent.py +0 -316
- schemas/__pycache__/schemas.cpython-314.pyc +0 -0
agents/__pycache__/__init__.cpython-314.pyc
CHANGED
|
Binary files a/agents/__pycache__/__init__.cpython-314.pyc and b/agents/__pycache__/__init__.cpython-314.pyc differ
|
|
|
agents/anomaly_agent.py
DELETED
|
@@ -1,415 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Agent 2: Anomaly Detection Agent
|
| 3 |
-
Выявляет аномалии и подозрительные паттерны в структурированных логах.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import re
|
| 7 |
-
import json
|
| 8 |
-
from typing import Dict, List, Any
|
| 9 |
-
from collections import defaultdict, Counter
|
| 10 |
-
from datetime import datetime
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class AnomalyDetectionAgent:
|
| 14 |
-
"""Обнаруживает аномалии в структурированных логах."""
|
| 15 |
-
|
| 16 |
-
def __init__(self):
|
| 17 |
-
"""Инициализация агента."""
|
| 18 |
-
self.burst_threshold = 5 # Минимальное количество ошибок для burst
|
| 19 |
-
self.burst_time_window = 60 # Окно времени в секундах для burst
|
| 20 |
-
self.repeat_threshold = 3 # Минимальное количество повторений
|
| 21 |
-
|
| 22 |
-
def detect(self, structured_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 23 |
-
"""
|
| 24 |
-
Выявляет аномалии в структурированных данных.
|
| 25 |
-
|
| 26 |
-
Args:
|
| 27 |
-
structured_data: Структурированные данные от LogParserAgent
|
| 28 |
-
|
| 29 |
-
Returns:
|
| 30 |
-
JSON-отчёт об аномалиях с описанием и метаданными
|
| 31 |
-
"""
|
| 32 |
-
if not structured_data or not structured_data.get('events'):
|
| 33 |
-
return self._empty_report()
|
| 34 |
-
|
| 35 |
-
events = structured_data.get('events', [])
|
| 36 |
-
errors = structured_data.get('errors', [])
|
| 37 |
-
|
| 38 |
-
anomalies = []
|
| 39 |
-
|
| 40 |
-
# 1. Обнаружение burst errors
|
| 41 |
-
burst_anomalies = self._detect_burst_errors(events, errors)
|
| 42 |
-
anomalies.extend(burst_anomalies)
|
| 43 |
-
|
| 44 |
-
# 2. Обнаружение повторяющихся ошибок
|
| 45 |
-
repeat_anomalies = self._detect_repeated_errors(errors)
|
| 46 |
-
anomalies.extend(repeat_anomalies)
|
| 47 |
-
|
| 48 |
-
# 3. Обнаружение паттернов "ошибка перед крашем"
|
| 49 |
-
crash_patterns = self._detect_error_before_crash(errors, events)
|
| 50 |
-
anomalies.extend(crash_patterns)
|
| 51 |
-
|
| 52 |
-
# 4. Обнаружение временных всплесков
|
| 53 |
-
spike_anomalies = self._detect_temporal_spikes(events)
|
| 54 |
-
anomalies.extend(spike_anomalies)
|
| 55 |
-
|
| 56 |
-
# 5. Обнаружение повторяющихся stack traces
|
| 57 |
-
stack_trace_anomalies = self._detect_repeated_stack_traces(events)
|
| 58 |
-
anomalies.extend(stack_trace_anomalies)
|
| 59 |
-
|
| 60 |
-
# Подсчёт статистики
|
| 61 |
-
anomaly_stats = self._calculate_anomaly_statistics(anomalies)
|
| 62 |
-
|
| 63 |
-
return {
|
| 64 |
-
'anomalies': anomalies,
|
| 65 |
-
'statistics': anomaly_stats,
|
| 66 |
-
'severity_summary': self._calculate_severity_summary(anomalies)
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
def _detect_burst_errors(self, events: List[Dict], errors: List[Dict]) -> List[Dict[str, Any]]:
|
| 70 |
-
"""Обнаруживает всплески ошибок (burst errors)."""
|
| 71 |
-
anomalies = []
|
| 72 |
-
|
| 73 |
-
if len(errors) < self.burst_threshold:
|
| 74 |
-
return anomalies
|
| 75 |
-
|
| 76 |
-
# Группировка ошибок по времени (если доступны временные метки)
|
| 77 |
-
error_times = []
|
| 78 |
-
for error in errors:
|
| 79 |
-
timestamp_str = error.get('timestamp')
|
| 80 |
-
if timestamp_str:
|
| 81 |
-
try:
|
| 82 |
-
timestamp = self._parse_timestamp_simple(timestamp_str)
|
| 83 |
-
if timestamp:
|
| 84 |
-
error_times.append((timestamp, error))
|
| 85 |
-
except:
|
| 86 |
-
pass
|
| 87 |
-
|
| 88 |
-
# Если временные метки доступны, анализируем временные окна
|
| 89 |
-
if error_times:
|
| 90 |
-
error_times.sort(key=lambda x: x[0] if x[0] else datetime.min)
|
| 91 |
-
|
| 92 |
-
# Поиск кластеров ошибок во временных окнах
|
| 93 |
-
i = 0
|
| 94 |
-
while i < len(error_times):
|
| 95 |
-
cluster_start = error_times[i][0]
|
| 96 |
-
cluster_errors = [error_times[i][1]]
|
| 97 |
-
|
| 98 |
-
j = i + 1
|
| 99 |
-
while j < len(error_times) and error_times[j][0]:
|
| 100 |
-
time_diff = (error_times[j][0] - cluster_start).total_seconds()
|
| 101 |
-
if time_diff <= self.burst_time_window:
|
| 102 |
-
cluster_errors.append(error_times[j][1])
|
| 103 |
-
j += 1
|
| 104 |
-
else:
|
| 105 |
-
break
|
| 106 |
-
|
| 107 |
-
if len(cluster_errors) >= self.burst_threshold:
|
| 108 |
-
messages = [e.get('message', '')[:100] for e in cluster_errors[:3]]
|
| 109 |
-
anomalies.append({
|
| 110 |
-
'type': 'BURST_ERRORS',
|
| 111 |
-
'severity': 'HIGH',
|
| 112 |
-
'description': f'Обнаружен всплеск из {len(cluster_errors)} ошибок в течение {self.burst_time_window} секунд',
|
| 113 |
-
'count': len(cluster_errors),
|
| 114 |
-
'time_window_seconds': self.burst_time_window,
|
| 115 |
-
'sample_messages': messages,
|
| 116 |
-
'first_occurrence': cluster_start.isoformat() if cluster_start else None,
|
| 117 |
-
'metadata': {
|
| 118 |
-
'threshold': self.burst_threshold,
|
| 119 |
-
'affected_lines': [e.get('line_number') for e in cluster_errors[:10]]
|
| 120 |
-
}
|
| 121 |
-
})
|
| 122 |
-
|
| 123 |
-
i = j
|
| 124 |
-
else:
|
| 125 |
-
# Если временных меток нет, проверяем последовательные ошибки
|
| 126 |
-
consecutive_count = 0
|
| 127 |
-
start_idx = 0
|
| 128 |
-
|
| 129 |
-
for i, error in enumerate(errors):
|
| 130 |
-
if i > 0:
|
| 131 |
-
prev_error = errors[i - 1]
|
| 132 |
-
# Проверяем, являются ли ошибки последовательными (по номерам строк)
|
| 133 |
-
if error.get('line_number', 0) - prev_error.get('line_number', 0) <= 5:
|
| 134 |
-
consecutive_count += 1
|
| 135 |
-
else:
|
| 136 |
-
if consecutive_count >= self.burst_threshold:
|
| 137 |
-
anomalies.append(self._create_burst_anomaly(
|
| 138 |
-
errors[start_idx:i], consecutive_count + 1
|
| 139 |
-
))
|
| 140 |
-
consecutive_count = 0
|
| 141 |
-
start_idx = i
|
| 142 |
-
else:
|
| 143 |
-
consecutive_count = 1
|
| 144 |
-
|
| 145 |
-
# Проверка последнего кластера
|
| 146 |
-
if consecutive_count >= self.burst_threshold:
|
| 147 |
-
anomalies.append(self._create_burst_anomaly(
|
| 148 |
-
errors[start_idx:], consecutive_count
|
| 149 |
-
))
|
| 150 |
-
|
| 151 |
-
return anomalies
|
| 152 |
-
|
| 153 |
-
def _create_burst_anomaly(self, errors: List[Dict], count: int) -> Dict[str, Any]:
|
| 154 |
-
"""Создаёт запись об аномалии burst errors."""
|
| 155 |
-
messages = [e.get('message', '')[:100] for e in errors[:3]]
|
| 156 |
-
return {
|
| 157 |
-
'type': 'BURST_ERRORS',
|
| 158 |
-
'severity': 'HIGH',
|
| 159 |
-
'description': f'Обнаружен всплеск из {count} последовательных ошибок',
|
| 160 |
-
'count': count,
|
| 161 |
-
'sample_messages': messages,
|
| 162 |
-
'metadata': {
|
| 163 |
-
'threshold': self.burst_threshold,
|
| 164 |
-
'affected_lines': [e.get('line_number') for e in errors[:10]]
|
| 165 |
-
}
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
def _detect_repeated_errors(self, errors: List[Dict]) -> List[Dict[str, Any]]:
|
| 169 |
-
"""Обнаруживает повторяющиеся ошибки."""
|
| 170 |
-
anomalies = []
|
| 171 |
-
|
| 172 |
-
if not errors:
|
| 173 |
-
return anomalies
|
| 174 |
-
|
| 175 |
-
# Группировка ошибок по сообщениям (нормализованным)
|
| 176 |
-
error_groups = defaultdict(list)
|
| 177 |
-
for error in errors:
|
| 178 |
-
message = self._normalize_message(error.get('message', ''))
|
| 179 |
-
error_groups[message].append(error)
|
| 180 |
-
|
| 181 |
-
# Поиск повторяющихся ошибок
|
| 182 |
-
for message, error_list in error_groups.items():
|
| 183 |
-
if len(error_list) >= self.repeat_threshold:
|
| 184 |
-
line_numbers = [e.get('line_number') for e in error_list]
|
| 185 |
-
timestamps = [e.get('timestamp') for e in error_list if e.get('timestamp')]
|
| 186 |
-
|
| 187 |
-
anomalies.append({
|
| 188 |
-
'type': 'REPEATED_ERRORS',
|
| 189 |
-
'severity': 'MEDIUM',
|
| 190 |
-
'description': f'Одна и та же ошибка повторяется {len(error_list)} раз(а)',
|
| 191 |
-
'count': len(error_list),
|
| 192 |
-
'error_message': message[:200],
|
| 193 |
-
'first_occurrence': timestamps[0] if timestamps else None,
|
| 194 |
-
'last_occurrence': timestamps[-1] if timestamps else None,
|
| 195 |
-
'metadata': {
|
| 196 |
-
'threshold': self.repeat_threshold,
|
| 197 |
-
'affected_lines': line_numbers[:20]
|
| 198 |
-
}
|
| 199 |
-
})
|
| 200 |
-
|
| 201 |
-
return anomalies
|
| 202 |
-
|
| 203 |
-
def _detect_error_before_crash(self, errors: List[Dict], events: List[Dict]) -> List[Dict[str, Any]]:
|
| 204 |
-
"""Обнаруживает паттерны "ошибка перед крашем"."""
|
| 205 |
-
anomalies = []
|
| 206 |
-
|
| 207 |
-
if not errors:
|
| 208 |
-
return anomalies
|
| 209 |
-
|
| 210 |
-
# Ищем последовательности критических ошибок в конце логов
|
| 211 |
-
# Или ошибки, за которыми следует остановка системы
|
| 212 |
-
crash_keywords = ['crash', 'shutdown', 'fatal', 'terminate', 'abort', 'exit']
|
| 213 |
-
|
| 214 |
-
# Проверяем последние события на наличие паттернов краша
|
| 215 |
-
last_events = events[-50:] if len(events) > 50 else events
|
| 216 |
-
last_errors = errors[-20:] if len(errors) > 20 else errors
|
| 217 |
-
|
| 218 |
-
for i, error in enumerate(last_errors):
|
| 219 |
-
error_msg_lower = error.get('message', '').lower()
|
| 220 |
-
error_level = error.get('level', '').upper()
|
| 221 |
-
|
| 222 |
-
# Проверяем, является ли это критической ошибкой
|
| 223 |
-
if error_level in ['CRITICAL', 'ERROR']:
|
| 224 |
-
# Проверяем последующие события на признаки краша
|
| 225 |
-
error_line = error.get('line_number', 0)
|
| 226 |
-
subsequent_events = [e for e in last_events if e.get('line_number', 0) > error_line][:10]
|
| 227 |
-
|
| 228 |
-
crash_indicators = []
|
| 229 |
-
for event in subsequent_events:
|
| 230 |
-
event_msg_lower = event.get('message', '').lower()
|
| 231 |
-
if any(keyword in event_msg_lower for keyword in crash_keywords):
|
| 232 |
-
crash_indicators.append(event.get('message', '')[:100])
|
| 233 |
-
|
| 234 |
-
if crash_indicators or i == len(last_errors) - 1:
|
| 235 |
-
anomalies.append({
|
| 236 |
-
'type': 'ERROR_BEFORE_CRASH',
|
| 237 |
-
'severity': 'CRITICAL',
|
| 238 |
-
'description': 'Обнаружен паттерн: ошибка перед возможным крашем системы',
|
| 239 |
-
'error_message': error.get('message', '')[:200],
|
| 240 |
-
'error_level': error_level,
|
| 241 |
-
'crash_indicators': crash_indicators[:3],
|
| 242 |
-
'metadata': {
|
| 243 |
-
'error_line': error_line,
|
| 244 |
-
'is_last_error': i == len(last_errors) - 1
|
| 245 |
-
}
|
| 246 |
-
})
|
| 247 |
-
|
| 248 |
-
return anomalies
|
| 249 |
-
|
| 250 |
-
def _detect_temporal_spikes(self, events: List[Dict]) -> List[Dict[str, Any]]:
|
| 251 |
-
"""Обнаруживает временные всплески событий."""
|
| 252 |
-
anomalies = []
|
| 253 |
-
|
| 254 |
-
# Группировка событий по времени (если доступны временные метки)
|
| 255 |
-
events_with_time = [(e.get('timestamp'), e) for e in events if e.get('timestamp')]
|
| 256 |
-
|
| 257 |
-
if len(events_with_time) < 10:
|
| 258 |
-
return anomalies
|
| 259 |
-
|
| 260 |
-
# Группировка по минутам (или другим временным окнам)
|
| 261 |
-
time_groups = defaultdict(list)
|
| 262 |
-
for timestamp_str, event in events_with_time:
|
| 263 |
-
try:
|
| 264 |
-
timestamp = self._parse_timestamp_simple(timestamp_str)
|
| 265 |
-
if timestamp:
|
| 266 |
-
# Группируем по минутам
|
| 267 |
-
time_key = timestamp.strftime('%Y-%m-%d %H:%M')
|
| 268 |
-
time_groups[time_key].append(event)
|
| 269 |
-
except:
|
| 270 |
-
pass
|
| 271 |
-
|
| 272 |
-
if not time_groups:
|
| 273 |
-
return anomalies
|
| 274 |
-
|
| 275 |
-
# Вычисляем среднее количество событий на временное окно
|
| 276 |
-
event_counts = [len(events) for events in time_groups.values()]
|
| 277 |
-
if not event_counts:
|
| 278 |
-
return anomalies
|
| 279 |
-
|
| 280 |
-
avg_count = sum(event_counts) / len(event_counts)
|
| 281 |
-
threshold = avg_count * 2 # Всплеск - это превышение среднего в 2 раза
|
| 282 |
-
|
| 283 |
-
# Поиск всплесков
|
| 284 |
-
for time_key, events_in_window in time_groups.items():
|
| 285 |
-
if len(events_in_window) > threshold:
|
| 286 |
-
error_count = len([e for e in events_in_window if e.get('level', '').upper() in ['ERROR', 'CRITICAL']])
|
| 287 |
-
|
| 288 |
-
anomalies.append({
|
| 289 |
-
'type': 'TEMPORAL_SPIKE',
|
| 290 |
-
'severity': 'MEDIUM',
|
| 291 |
-
'description': f'Обнаружен временной всплеск: {len(events_in_window)} событий за период {time_key} (среднее: {avg_count:.1f})',
|
| 292 |
-
'time_window': time_key,
|
| 293 |
-
'event_count': len(events_in_window),
|
| 294 |
-
'average_count': round(avg_count, 1),
|
| 295 |
-
'error_count': error_count,
|
| 296 |
-
'metadata': {
|
| 297 |
-
'threshold_multiplier': 2.0
|
| 298 |
-
}
|
| 299 |
-
})
|
| 300 |
-
|
| 301 |
-
return anomalies
|
| 302 |
-
|
| 303 |
-
def _detect_repeated_stack_traces(self, events: List[Dict]) -> List[Dict[str, Any]]:
|
| 304 |
-
"""Обнаруживает повторяющиеся stack traces."""
|
| 305 |
-
anomalies = []
|
| 306 |
-
|
| 307 |
-
# Ищем строки, похожие на stack traces
|
| 308 |
-
stack_trace_keywords = ['traceback', 'stack trace', 'at ', 'exception', 'file "', 'line ', 'in ']
|
| 309 |
-
potential_stacks = []
|
| 310 |
-
|
| 311 |
-
for event in events:
|
| 312 |
-
message = event.get('message', '').lower()
|
| 313 |
-
if any(keyword in message for keyword in stack_trace_keywords):
|
| 314 |
-
# Проверяем длину сообщения (stack traces обычно длинные)
|
| 315 |
-
if len(event.get('message', '')) > 100:
|
| 316 |
-
potential_stacks.append(event)
|
| 317 |
-
|
| 318 |
-
if len(potential_stacks) < self.repeat_threshold:
|
| 319 |
-
return anomalies
|
| 320 |
-
|
| 321 |
-
# Группировка по нормализованным сообщениям
|
| 322 |
-
stack_groups = defaultdict(list)
|
| 323 |
-
for stack in potential_stacks:
|
| 324 |
-
normalized = self._normalize_stack_trace(stack.get('message', ''))
|
| 325 |
-
stack_groups[normalized].append(stack)
|
| 326 |
-
|
| 327 |
-
# Поиск повторяющихся
|
| 328 |
-
for normalized_stack, stack_list in stack_groups.items():
|
| 329 |
-
if len(stack_list) >= self.repeat_threshold:
|
| 330 |
-
anomalies.append({
|
| 331 |
-
'type': 'REPEATED_STACK_TRACES',
|
| 332 |
-
'severity': 'HIGH',
|
| 333 |
-
'description': f'Один и тот же stack trace повторяется {len(stack_list)} раз(а)',
|
| 334 |
-
'count': len(stack_list),
|
| 335 |
-
'stack_trace_preview': normalized_stack[:300],
|
| 336 |
-
'metadata': {
|
| 337 |
-
'threshold': self.repeat_threshold,
|
| 338 |
-
'affected_lines': [s.get('line_number') for s in stack_list[:10]]
|
| 339 |
-
}
|
| 340 |
-
})
|
| 341 |
-
|
| 342 |
-
return anomalies
|
| 343 |
-
|
| 344 |
-
def _normalize_message(self, message: str) -> str:
|
| 345 |
-
"""Нормализует сообщение для группировки (удаляет переменные части)."""
|
| 346 |
-
# Удаляем числа и даты
|
| 347 |
-
normalized = re.sub(r'\d+', 'N', message)
|
| 348 |
-
# Удаляем пути к файлам
|
| 349 |
-
normalized = re.sub(r'[A-Z]:\\[^\s]+|/[^\s]+', 'PATH', normalized)
|
| 350 |
-
# Удаляем URL
|
| 351 |
-
normalized = re.sub(r'https?://[^\s]+', 'URL', normalized)
|
| 352 |
-
return normalized.strip()
|
| 353 |
-
|
| 354 |
-
def _normalize_stack_trace(self, stack: str) -> str:
|
| 355 |
-
"""Нормализует stack trace для сравнения."""
|
| 356 |
-
# Оставляем только ключевые части stack trace
|
| 357 |
-
lines = stack.split('\n')[:5] # Первые 5 строк обычно достаточны
|
| 358 |
-
normalized = '\n'.join([line.strip() for line in lines])
|
| 359 |
-
# Удаляем пути и номера строк
|
| 360 |
-
normalized = re.sub(r'File "[^"]+", line \d+', 'File "FILE", line N', normalized)
|
| 361 |
-
return normalized
|
| 362 |
-
|
| 363 |
-
def _parse_timestamp_simple(self, timestamp_str: str) -> datetime | None:
|
| 364 |
-
"""Простой парсер временных меток."""
|
| 365 |
-
timestamp_str = timestamp_str.strip('[]')
|
| 366 |
-
formats = [
|
| 367 |
-
'%Y-%m-%d %H:%M:%S',
|
| 368 |
-
'%Y-%m-%dT%H:%M:%S',
|
| 369 |
-
'%Y-%m-%d %H:%M:%S.%f',
|
| 370 |
-
'%Y-%m-%dT%H:%M:%S.%f',
|
| 371 |
-
'%d/%m/%Y %H:%M:%S',
|
| 372 |
-
]
|
| 373 |
-
|
| 374 |
-
for fmt in formats:
|
| 375 |
-
try:
|
| 376 |
-
return datetime.strptime(timestamp_str, fmt)
|
| 377 |
-
except ValueError:
|
| 378 |
-
continue
|
| 379 |
-
|
| 380 |
-
return None
|
| 381 |
-
|
| 382 |
-
def _calculate_anomaly_statistics(self, anomalies: List[Dict]) -> Dict[str, Any]:
|
| 383 |
-
"""Вычисляет статистику аномалий."""
|
| 384 |
-
if not anomalies:
|
| 385 |
-
return {
|
| 386 |
-
'total': 0,
|
| 387 |
-
'by_type': {},
|
| 388 |
-
'by_severity': {}
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
by_type = Counter(a.get('type') for a in anomalies)
|
| 392 |
-
by_severity = Counter(a.get('severity') for a in anomalies)
|
| 393 |
-
|
| 394 |
-
return {
|
| 395 |
-
'total': len(anomalies),
|
| 396 |
-
'by_type': dict(by_type),
|
| 397 |
-
'by_severity': dict(by_severity)
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
def _calculate_severity_summary(self, anomalies: List[Dict]) -> Dict[str, int]:
|
| 401 |
-
"""Вычисляет сводку по уровням серьёзности."""
|
| 402 |
-
severity_counts = Counter(a.get('severity', 'UNKNOWN') for a in anomalies)
|
| 403 |
-
return dict(severity_counts)
|
| 404 |
-
|
| 405 |
-
def _empty_report(self) -> Dict[str, Any]:
|
| 406 |
-
"""Возвращает пустой отчёт при отсутствии данных."""
|
| 407 |
-
return {
|
| 408 |
-
'anomalies': [],
|
| 409 |
-
'statistics': {
|
| 410 |
-
'total': 0,
|
| 411 |
-
'by_type': {},
|
| 412 |
-
'by_severity': {}
|
| 413 |
-
},
|
| 414 |
-
'severity_summary': {}
|
| 415 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/parser_agent.py
DELETED
|
@@ -1,217 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Agent 1: Log Parser Agent
|
| 3 |
-
Преобразует сырые логи в структурированное представление.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import re
|
| 7 |
-
import json
|
| 8 |
-
from datetime import datetime
|
| 9 |
-
from typing import Dict, List, Any
|
| 10 |
-
from collections import defaultdict
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class LogParserAgent:
|
| 14 |
-
"""Парсит сырые логи и преобразует их в структурированный JSON."""
|
| 15 |
-
|
| 16 |
-
# Паттерны для распознавания уровней логирования
|
| 17 |
-
LOG_LEVELS = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE']
|
| 18 |
-
|
| 19 |
-
# Паттерны для временных меток (поддержка различных форматов)
|
| 20 |
-
TIMESTAMP_PATTERNS = [
|
| 21 |
-
r'\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}(?:\.\d+)?', # ISO 8601
|
| 22 |
-
r'\d{2}/\d{2}/\d{4}[\s]\d{2}:\d{2}:\d{2}', # DD/MM/YYYY HH:MM:SS
|
| 23 |
-
r'\[(?:[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\]', # [Mon Jan 1 12:00:00]
|
| 24 |
-
]
|
| 25 |
-
|
| 26 |
-
def __init__(self):
|
| 27 |
-
"""Инициализация агента."""
|
| 28 |
-
self.compiled_patterns = [re.compile(pattern) for pattern in self.TIMESTAMP_PATTERNS]
|
| 29 |
-
|
| 30 |
-
def parse(self, raw_logs: str) -> Dict[str, Any]:
|
| 31 |
-
"""
|
| 32 |
-
Парсит сырые логи и возвращает структурированный JSON.
|
| 33 |
-
|
| 34 |
-
Args:
|
| 35 |
-
raw_logs: Строка с сырыми логами
|
| 36 |
-
|
| 37 |
-
Returns:
|
| 38 |
-
Структурированный JSON-объект с событиями, ошибками, предупреждениями и статистикой
|
| 39 |
-
"""
|
| 40 |
-
if not raw_logs or not raw_logs.strip():
|
| 41 |
-
return self._empty_result()
|
| 42 |
-
|
| 43 |
-
lines = raw_logs.strip().split('\n')
|
| 44 |
-
events = []
|
| 45 |
-
errors = []
|
| 46 |
-
warnings = []
|
| 47 |
-
|
| 48 |
-
for line_num, line in enumerate(lines, start=1):
|
| 49 |
-
if not line.strip():
|
| 50 |
-
continue
|
| 51 |
-
|
| 52 |
-
parsed_event = self._parse_line(line, line_num)
|
| 53 |
-
if parsed_event:
|
| 54 |
-
events.append(parsed_event)
|
| 55 |
-
|
| 56 |
-
level = parsed_event.get('level', '').upper()
|
| 57 |
-
if level == 'ERROR' or level == 'CRITICAL':
|
| 58 |
-
errors.append(parsed_event)
|
| 59 |
-
elif level == 'WARNING':
|
| 60 |
-
warnings.append(parsed_event)
|
| 61 |
-
|
| 62 |
-
# Группировка по типам событий
|
| 63 |
-
event_types = defaultdict(int)
|
| 64 |
-
for event in events:
|
| 65 |
-
event_type = event.get('type', 'UNKNOWN')
|
| 66 |
-
event_types[event_type] += 1
|
| 67 |
-
|
| 68 |
-
# Статистика
|
| 69 |
-
statistics = {
|
| 70 |
-
'total_lines': len(lines),
|
| 71 |
-
'parsed_events': len(events),
|
| 72 |
-
'errors': len(errors),
|
| 73 |
-
'warnings': len(warnings),
|
| 74 |
-
'info_messages': len([e for e in events if e.get('level', '').upper() == 'INFO']),
|
| 75 |
-
'event_types': dict(event_types),
|
| 76 |
-
'time_range': self._calculate_time_range(events),
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
return {
|
| 80 |
-
'events': events,
|
| 81 |
-
'errors': errors,
|
| 82 |
-
'warnings': warnings,
|
| 83 |
-
'statistics': statistics
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
def _parse_line(self, line: str, line_num: int) -> Dict[str, Any] | None:
|
| 87 |
-
"""
|
| 88 |
-
Парсит одну строку лога.
|
| 89 |
-
|
| 90 |
-
Args:
|
| 91 |
-
line: Строка лога
|
| 92 |
-
line_num: Номер строки
|
| 93 |
-
|
| 94 |
-
Returns:
|
| 95 |
-
Словарь с распарсенными данными или None
|
| 96 |
-
"""
|
| 97 |
-
# Поиск временной метки
|
| 98 |
-
timestamp = None
|
| 99 |
-
timestamp_str = None
|
| 100 |
-
for pattern in self.compiled_patterns:
|
| 101 |
-
match = pattern.search(line)
|
| 102 |
-
if match:
|
| 103 |
-
timestamp_str = match.group(0)
|
| 104 |
-
try:
|
| 105 |
-
# Попытка парсинга различных форматов
|
| 106 |
-
timestamp = self._parse_timestamp(timestamp_str)
|
| 107 |
-
except:
|
| 108 |
-
pass
|
| 109 |
-
break
|
| 110 |
-
|
| 111 |
-
# Поиск уровня логирования
|
| 112 |
-
level = None
|
| 113 |
-
for log_level in self.LOG_LEVELS:
|
| 114 |
-
if log_level in line.upper():
|
| 115 |
-
level = log_level
|
| 116 |
-
break
|
| 117 |
-
|
| 118 |
-
# Если уровень не найден, определяем по ключевым словам
|
| 119 |
-
if not level:
|
| 120 |
-
line_upper = line.upper()
|
| 121 |
-
if any(word in line_upper for word in ['ERROR', 'EXCEPTION', 'FAILED', 'FAILURE']):
|
| 122 |
-
level = 'ERROR'
|
| 123 |
-
elif any(word in line_upper for word in ['WARN', 'WARNING']):
|
| 124 |
-
level = 'WARNING'
|
| 125 |
-
elif any(word in line_upper for word in ['INFO', 'INFORMATION']):
|
| 126 |
-
level = 'INFO'
|
| 127 |
-
elif any(word in line_upper for word in ['DEBUG']):
|
| 128 |
-
level = 'DEBUG'
|
| 129 |
-
else:
|
| 130 |
-
level = 'INFO' # По умолчанию
|
| 131 |
-
|
| 132 |
-
# Извлечение сообщения (часть после временной метки и уровня)
|
| 133 |
-
message = line
|
| 134 |
-
if timestamp_str:
|
| 135 |
-
message = message.replace(timestamp_str, '', 1).strip()
|
| 136 |
-
|
| 137 |
-
# Определение типа события
|
| 138 |
-
event_type = self._detect_event_type(line)
|
| 139 |
-
|
| 140 |
-
return {
|
| 141 |
-
'line_number': line_num,
|
| 142 |
-
'timestamp': timestamp_str if timestamp_str else None,
|
| 143 |
-
'level': level,
|
| 144 |
-
'message': message.strip(),
|
| 145 |
-
'type': event_type,
|
| 146 |
-
'raw': line
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
def _parse_timestamp(self, timestamp_str: str) -> datetime | None:
|
| 150 |
-
"""Парсит строку временной метки в объект datetime."""
|
| 151 |
-
# Удаление скобок если есть
|
| 152 |
-
timestamp_str = timestamp_str.strip('[]')
|
| 153 |
-
|
| 154 |
-
# Попытка различных форматов
|
| 155 |
-
formats = [
|
| 156 |
-
'%Y-%m-%d %H:%M:%S',
|
| 157 |
-
'%Y-%m-%dT%H:%M:%S',
|
| 158 |
-
'%Y-%m-%d %H:%M:%S.%f',
|
| 159 |
-
'%Y-%m-%dT%H:%M:%S.%f',
|
| 160 |
-
'%d/%m/%Y %H:%M:%S',
|
| 161 |
-
'%a %b %d %H:%M:%S %Y', # [Mon Jan 1 12:00:00 2024]
|
| 162 |
-
]
|
| 163 |
-
|
| 164 |
-
for fmt in formats:
|
| 165 |
-
try:
|
| 166 |
-
return datetime.strptime(timestamp_str, fmt)
|
| 167 |
-
except ValueError:
|
| 168 |
-
continue
|
| 169 |
-
|
| 170 |
-
return None
|
| 171 |
-
|
| 172 |
-
def _detect_event_type(self, line: str) -> str:
|
| 173 |
-
"""Определяет тип события по содержимому строки."""
|
| 174 |
-
line_lower = line.lower()
|
| 175 |
-
|
| 176 |
-
if any(keyword in line_lower for keyword in ['connection', 'connect', 'disconnect']):
|
| 177 |
-
return 'CONNECTION'
|
| 178 |
-
elif any(keyword in line_lower for keyword in ['request', 'response', 'http', 'api']):
|
| 179 |
-
return 'HTTP_REQUEST'
|
| 180 |
-
elif any(keyword in line_lower for keyword in ['database', 'db', 'query', 'sql']):
|
| 181 |
-
return 'DATABASE'
|
| 182 |
-
elif any(keyword in line_lower for keyword in ['authentication', 'auth', 'login', 'logout']):
|
| 183 |
-
return 'AUTHENTICATION'
|
| 184 |
-
elif any(keyword in line_lower for keyword in ['exception', 'error', 'failure']):
|
| 185 |
-
return 'EXCEPTION'
|
| 186 |
-
elif any(keyword in line_lower for keyword in ['start', 'stop', 'shutdown', 'initialized']):
|
| 187 |
-
return 'SYSTEM'
|
| 188 |
-
else:
|
| 189 |
-
return 'GENERAL'
|
| 190 |
-
|
| 191 |
-
def _calculate_time_range(self, events: List[Dict[str, Any]]) -> Dict[str, str] | None:
|
| 192 |
-
"""Вычисляет временной диапазон событий."""
|
| 193 |
-
timestamps = [e.get('timestamp') for e in events if e.get('timestamp')]
|
| 194 |
-
if not timestamps:
|
| 195 |
-
return None
|
| 196 |
-
|
| 197 |
-
return {
|
| 198 |
-
'start': timestamps[0],
|
| 199 |
-
'end': timestamps[-1]
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
def _empty_result(self) -> Dict[str, Any]:
|
| 203 |
-
"""Возвращает пустой результат при отсутствии логов."""
|
| 204 |
-
return {
|
| 205 |
-
'events': [],
|
| 206 |
-
'errors': [],
|
| 207 |
-
'warnings': [],
|
| 208 |
-
'statistics': {
|
| 209 |
-
'total_lines': 0,
|
| 210 |
-
'parsed_events': 0,
|
| 211 |
-
'errors': 0,
|
| 212 |
-
'warnings': 0,
|
| 213 |
-
'info_messages': 0,
|
| 214 |
-
'event_types': {},
|
| 215 |
-
'time_range': None
|
| 216 |
-
}
|
| 217 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/rca_agent.py
DELETED
|
@@ -1,316 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Agent 3: Root Cause & Recommendation Agent
|
| 3 |
-
Интерпретирует аномалии и формирует рекомендации.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import Dict, List, Any
|
| 7 |
-
import json
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
class RootCauseAgent:
|
| 11 |
-
"""Анализирует аномалии и генерирует рекомендации."""
|
| 12 |
-
|
| 13 |
-
def __init__(self):
|
| 14 |
-
"""Инициализация агента."""
|
| 15 |
-
self.root_cause_templates = self._init_root_cause_templates()
|
| 16 |
-
self.recommendation_templates = self._init_recommendation_templates()
|
| 17 |
-
|
| 18 |
-
def analyze(self, anomaly_report: Dict[str, Any]) -> str:
|
| 19 |
-
"""
|
| 20 |
-
Анализирует отчёт об аномалиях и генерирует рекомендации.
|
| 21 |
-
|
| 22 |
-
Args:
|
| 23 |
-
anomaly_report: Отчёт об аномалиях от AnomalyDetectionAgent
|
| 24 |
-
|
| 25 |
-
Returns:
|
| 26 |
-
Markdown-текст с анализом и рекомендациями
|
| 27 |
-
"""
|
| 28 |
-
if not anomaly_report or not anomaly_report.get('anomalies'):
|
| 29 |
-
return self._generate_no_anomalies_report()
|
| 30 |
-
|
| 31 |
-
anomalies = anomaly_report.get('anomalies', [])
|
| 32 |
-
statistics = anomaly_report.get('statistics', {})
|
| 33 |
-
severity_summary = anomaly_report.get('severity_summary', {})
|
| 34 |
-
|
| 35 |
-
# Генерация отчёта
|
| 36 |
-
report_parts = []
|
| 37 |
-
|
| 38 |
-
# Заголовок
|
| 39 |
-
report_parts.append("# Анализ первопричин и рекомендации\n")
|
| 40 |
-
report_parts.append(f"**Обнаружено аномалий:** {statistics.get('total', 0)}\n")
|
| 41 |
-
|
| 42 |
-
# Сводка по серьёзности
|
| 43 |
-
if severity_summary:
|
| 44 |
-
report_parts.append("\n## Сводка по уровням серьёзности\n")
|
| 45 |
-
severity_order = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
|
| 46 |
-
for severity in severity_order:
|
| 47 |
-
count = severity_summary.get(severity, 0)
|
| 48 |
-
if count > 0:
|
| 49 |
-
emoji = self._get_severity_emoji(severity)
|
| 50 |
-
report_parts.append(f"- {emoji} **{severity}:** {count}\n")
|
| 51 |
-
|
| 52 |
-
# Группировка аномалий по типам
|
| 53 |
-
anomalies_by_type = {}
|
| 54 |
-
for anomaly in anomalies:
|
| 55 |
-
anomaly_type = anomaly.get('type', 'UNKNOWN')
|
| 56 |
-
if anomaly_type not in anomalies_by_type:
|
| 57 |
-
anomalies_by_type[anomaly_type] = []
|
| 58 |
-
anomalies_by_type[anomaly_type].append(anomaly)
|
| 59 |
-
|
| 60 |
-
# Анализ каждого типа аномалий
|
| 61 |
-
report_parts.append("\n## Детальный анализ аномалий\n")
|
| 62 |
-
|
| 63 |
-
for anomaly_type, type_anomalies in anomalies_by_type.items():
|
| 64 |
-
report_parts.append(f"\n### {self._get_anomaly_type_name(anomaly_type)}\n")
|
| 65 |
-
|
| 66 |
-
# Анализ первопричин
|
| 67 |
-
root_causes = self._identify_root_causes(anomaly_type, type_anomalies)
|
| 68 |
-
if root_causes:
|
| 69 |
-
report_parts.append("#### Возможные первопричины:\n")
|
| 70 |
-
for i, cause in enumerate(root_causes, 1):
|
| 71 |
-
report_parts.append(f"{i}. {cause}\n")
|
| 72 |
-
|
| 73 |
-
# Детали аномалий
|
| 74 |
-
report_parts.append("\n#### Детали:\n")
|
| 75 |
-
for i, anomaly in enumerate(type_anomalies[:5], 1): # Показываем до 5 примеров
|
| 76 |
-
severity = anomaly.get('severity', 'UNKNOWN')
|
| 77 |
-
description = anomaly.get('description', 'Без описания')
|
| 78 |
-
report_parts.append(f"**Аномалия {i}** ({severity}):\n")
|
| 79 |
-
report_parts.append(f"- {description}\n")
|
| 80 |
-
|
| 81 |
-
# Дополнительная информация
|
| 82 |
-
if anomaly.get('count'):
|
| 83 |
-
report_parts.append(f"- Количество: {anomaly.get('count')}\n")
|
| 84 |
-
if anomaly.get('error_message'):
|
| 85 |
-
error_msg = anomaly.get('error_message', '')[:150]
|
| 86 |
-
report_parts.append(f"- Сообщение: `{error_msg}`\n")
|
| 87 |
-
if anomaly.get('metadata'):
|
| 88 |
-
metadata = anomaly.get('metadata', {})
|
| 89 |
-
if metadata.get('affected_lines'):
|
| 90 |
-
lines = metadata.get('affected_lines', [])[:5]
|
| 91 |
-
report_parts.append(f"- Затронутые строки: {', '.join(map(str, lines))}\n")
|
| 92 |
-
|
| 93 |
-
if len(type_anomalies) > 5:
|
| 94 |
-
report_parts.append(f"\n*... и ещё {len(type_anomalies) - 5} аномалий этого типа*\n")
|
| 95 |
-
|
| 96 |
-
# Рекомендации
|
| 97 |
-
report_parts.append("\n## Рекомендации по устранению\n")
|
| 98 |
-
recommendations = self._generate_recommendations(anomalies)
|
| 99 |
-
|
| 100 |
-
for i, recommendation in enumerate(recommendations, 1):
|
| 101 |
-
priority = recommendation.get('priority', 'MEDIUM')
|
| 102 |
-
emoji = self._get_priority_emoji(priority)
|
| 103 |
-
report_parts.append(f"\n### {emoji} Рекомендация {i} (Приоритет: {priority})\n")
|
| 104 |
-
report_parts.append(f"{recommendation.get('text', '')}\n")
|
| 105 |
-
|
| 106 |
-
if recommendation.get('actions'):
|
| 107 |
-
report_parts.append("**Конкретные действия:**\n")
|
| 108 |
-
for action in recommendation.get('actions', []):
|
| 109 |
-
report_parts.append(f"- {action}\n")
|
| 110 |
-
|
| 111 |
-
# Общие рекомендации
|
| 112 |
-
report_parts.append("\n## Общие рекомендации\n")
|
| 113 |
-
general_recommendations = self._generate_general_recommendations(anomalies, statistics)
|
| 114 |
-
for rec in general_recommendations:
|
| 115 |
-
report_parts.append(f"- {rec}\n")
|
| 116 |
-
|
| 117 |
-
return ''.join(report_parts)
|
| 118 |
-
|
| 119 |
-
def _identify_root_causes(self, anomaly_type: str, anomalies: List[Dict]) -> List[str]:
|
| 120 |
-
"""Определяет возможные первопричины для типа аномалий."""
|
| 121 |
-
causes = []
|
| 122 |
-
|
| 123 |
-
if anomaly_type == 'BURST_ERRORS':
|
| 124 |
-
causes.extend([
|
| 125 |
-
"Внезапная перегрузка системы или внешнего сервиса",
|
| 126 |
-
"Сбой в инфраструктуре (сеть, база данных, диск)",
|
| 127 |
-
"Проблемы с зависимыми сервисами или API",
|
| 128 |
-
"Некорректное обновление или развертывание кода"
|
| 129 |
-
])
|
| 130 |
-
elif anomaly_type == 'REPEATED_ERRORS':
|
| 131 |
-
causes.extend([
|
| 132 |
-
"Проблема в коде, которая воспроизводится при определённых условиях",
|
| 133 |
-
"Недостаточная обработка ошибок в цикле или повторяющемся процессе",
|
| 134 |
-
"Проблема конфигурации, влияющая на конкретную функциональность",
|
| 135 |
-
"Ресурсные ограничения (память, диск, соединения)"
|
| 136 |
-
])
|
| 137 |
-
elif anomaly_type == 'ERROR_BEFORE_CRASH':
|
| 138 |
-
causes.extend([
|
| 139 |
-
"Критическая ошибка, приводящая к падению процесса",
|
| 140 |
-
"Исчерпание ресурсов (память, дескрипторы файлов)",
|
| 141 |
-
"Некорректное состояние приложения после длительной работы",
|
| 142 |
-
"Проблемы с внешними зависимостями"
|
| 143 |
-
])
|
| 144 |
-
elif anomaly_type == 'TEMPORAL_SPIKE':
|
| 145 |
-
causes.extend([
|
| 146 |
-
"Плановые задачи (cron jobs, scheduled tasks)",
|
| 147 |
-
"Резкое увеличение нагрузки от пользователей",
|
| 148 |
-
"Внешние события, вызывающие массовые запросы",
|
| 149 |
-
"Проблемы с кэшированием или сессиями"
|
| 150 |
-
])
|
| 151 |
-
elif anomaly_type == 'REPEATED_STACK_TRACES':
|
| 152 |
-
causes.extend([
|
| 153 |
-
"Необработанное исключение в часто вызываемом коде",
|
| 154 |
-
"Проблема в библиотеке или зависимостях",
|
| 155 |
-
"Некорректные входные данные, вызывающие исключение",
|
| 156 |
-
"Race condition или проблема конкурентности"
|
| 157 |
-
])
|
| 158 |
-
else:
|
| 159 |
-
causes.append("Требуется дополнительный анализ для определения первопричины")
|
| 160 |
-
|
| 161 |
-
return causes
|
| 162 |
-
|
| 163 |
-
def _generate_recommendations(self, anomalies: List[Dict]) -> List[Dict[str, Any]]:
|
| 164 |
-
"""Генерирует рекомендации на основе обнаруженных аномалий."""
|
| 165 |
-
recommendations = []
|
| 166 |
-
|
| 167 |
-
# Группировка по типам для приоритизации
|
| 168 |
-
anomaly_types = [a.get('type') for a in anomalies]
|
| 169 |
-
severities = [a.get('severity') for a in anomalies]
|
| 170 |
-
|
| 171 |
-
has_critical = any(s == 'CRITICAL' for s in severities)
|
| 172 |
-
has_high = any(s == 'HIGH' for s in severities)
|
| 173 |
-
has_burst = 'BURST_ERRORS' in anomaly_types
|
| 174 |
-
has_crash = 'ERROR_BEFORE_CRASH' in anomaly_types
|
| 175 |
-
|
| 176 |
-
# Критические рекомендации
|
| 177 |
-
if has_crash:
|
| 178 |
-
recommendations.append({
|
| 179 |
-
'priority': 'CRITICAL',
|
| 180 |
-
'text': 'Обнаружены признаки возможн��го краша системы. Требуется немедленное внимание.',
|
| 181 |
-
'actions': [
|
| 182 |
-
'Проверить состояние системы и процессов',
|
| 183 |
-
'Проанализировать последние ошибки перед крашем',
|
| 184 |
-
'Убедиться, что мониторинг и алертинг настроены корректно',
|
| 185 |
-
'Рассмотреть возможность отката последних изменений'
|
| 186 |
-
]
|
| 187 |
-
})
|
| 188 |
-
|
| 189 |
-
if has_burst:
|
| 190 |
-
recommendations.append({
|
| 191 |
-
'priority': 'HIGH',
|
| 192 |
-
'text': 'Обнаружены всплески ошибок. Необходимо определить источник нагрузки.',
|
| 193 |
-
'actions': [
|
| 194 |
-
'Проверить метрики нагрузки (CPU, память, сеть)',
|
| 195 |
-
'Изучить логи зависимых сервисов',
|
| 196 |
-
'Проверить состояние базы данных и внешних API',
|
| 197 |
-
'Рассмотреть возможность масштабирования или rate limiting'
|
| 198 |
-
]
|
| 199 |
-
})
|
| 200 |
-
|
| 201 |
-
# Рекомендации по повторяющимся ошибкам
|
| 202 |
-
if 'REPEATED_ERRORS' in anomaly_types:
|
| 203 |
-
recommendations.append({
|
| 204 |
-
'priority': 'HIGH',
|
| 205 |
-
'text': 'Обнаружены повторяющиеся ошибки. Требуется исправление в коде или конфигурации.',
|
| 206 |
-
'actions': [
|
| 207 |
-
'Идентифицировать конкретный участок кода, вызывающий ошибку',
|
| 208 |
-
'Добавить более детальное логирование для отладки',
|
| 209 |
-
'Улучшить обработку ошибок с логированием контекста',
|
| 210 |
-
'Провести code review проблемного участка'
|
| 211 |
-
]
|
| 212 |
-
})
|
| 213 |
-
|
| 214 |
-
# Рекомендации по stack traces
|
| 215 |
-
if 'REPEATED_STACK_TRACES' in anomaly_types:
|
| 216 |
-
recommendations.append({
|
| 217 |
-
'priority': 'MEDIUM',
|
| 218 |
-
'text': 'Обнаружены повторяющиеся stack traces. Необходимо исправить необработанные исключения.',
|
| 219 |
-
'actions': [
|
| 220 |
-
'Найти и исправить источник исключения',
|
| 221 |
-
'Добавить обработку исключений (try-except блоки)',
|
| 222 |
-
'Улучшить валидацию входных данных',
|
| 223 |
-
'Обновить проблемные библиотеки или зависимости'
|
| 224 |
-
]
|
| 225 |
-
})
|
| 226 |
-
|
| 227 |
-
# Общие рекомендации по мониторингу
|
| 228 |
-
if has_high or has_critical:
|
| 229 |
-
recommendations.append({
|
| 230 |
-
'priority': 'MEDIUM',
|
| 231 |
-
'text': 'Улучшить систему мониторинга и алертинга для раннего обнаружения проблем.',
|
| 232 |
-
'actions': [
|
| 233 |
-
'Настроить алерты на критические ошибки',
|
| 234 |
-
'Внедрить мониторинг метрик производительности',
|
| 235 |
-
'Настроить дашборды для визуализации состояния системы',
|
| 236 |
-
'Реализовать автоматические проверки здоровья (health checks)'
|
| 237 |
-
]
|
| 238 |
-
})
|
| 239 |
-
|
| 240 |
-
return recommendations
|
| 241 |
-
|
| 242 |
-
def _generate_general_recommendations(self, anomalies: List[Dict], statistics: Dict) -> List[str]:
|
| 243 |
-
"""Генерирует общие рекомендации."""
|
| 244 |
-
recommendations = []
|
| 245 |
-
|
| 246 |
-
total = statistics.get('total', 0)
|
| 247 |
-
if total == 0:
|
| 248 |
-
return ["Логи не содержат аномалий. Система работает стабильно."]
|
| 249 |
-
|
| 250 |
-
recommendations.append("Регулярно проверяйте логи на наличие паттернов и трендов")
|
| 251 |
-
recommendations.append("Настройте автоматическое уведомление о критических ошибках")
|
| 252 |
-
|
| 253 |
-
if total > 10:
|
| 254 |
-
recommendations.append("Обнаружено значительное количество аномалий - рекомендуется провести комплексный анализ системы")
|
| 255 |
-
|
| 256 |
-
recommendations.append("Ведите документацию по известным проблемам и их решениям")
|
| 257 |
-
recommendations.append("Рассмотрите возможность внедрения централизованного логирования (ELK, Splunk и т.д.)")
|
| 258 |
-
|
| 259 |
-
return recommendations
|
| 260 |
-
|
| 261 |
-
def _get_anomaly_type_name(self, anomaly_type: str) -> str:
|
| 262 |
-
"""Возвращает читаемое название типа аномалии."""
|
| 263 |
-
names = {
|
| 264 |
-
'BURST_ERRORS': 'Всплески ошибок',
|
| 265 |
-
'REPEATED_ERRORS': 'Повторяющиеся ошибки',
|
| 266 |
-
'ERROR_BEFORE_CRASH': 'Ошибки перед крашем',
|
| 267 |
-
'TEMPORAL_SPIKE': 'Временные всплески',
|
| 268 |
-
'REPEATED_STACK_TRACES': 'Повторяющиеся stack traces'
|
| 269 |
-
}
|
| 270 |
-
return names.get(anomaly_type, anomaly_type)
|
| 271 |
-
|
| 272 |
-
def _get_severity_emoji(self, severity: str) -> str:
|
| 273 |
-
"""Возвращает emoji для уровня серьёзности."""
|
| 274 |
-
emoji_map = {
|
| 275 |
-
'CRITICAL': '🔴',
|
| 276 |
-
'HIGH': '🟠',
|
| 277 |
-
'MEDIUM': '🟡',
|
| 278 |
-
'LOW': '🟢'
|
| 279 |
-
}
|
| 280 |
-
return emoji_map.get(severity, '⚪')
|
| 281 |
-
|
| 282 |
-
def _get_priority_emoji(self, priority: str) -> str:
|
| 283 |
-
"""Возвращает emoji для приоритета."""
|
| 284 |
-
emoji_map = {
|
| 285 |
-
'CRITICAL': '🔴',
|
| 286 |
-
'HIGH': '🟠',
|
| 287 |
-
'MEDIUM': '🟡',
|
| 288 |
-
'LOW': '🟢'
|
| 289 |
-
}
|
| 290 |
-
return emoji_map.get(priority, '⚪')
|
| 291 |
-
|
| 292 |
-
def _init_root_cause_templates(self) -> Dict[str, List[str]]:
|
| 293 |
-
"""Инициализирует шаблоны первопричин."""
|
| 294 |
-
return {}
|
| 295 |
-
|
| 296 |
-
def _init_recommendation_templates(self) -> Dict[str, List[str]]:
|
| 297 |
-
"""Инициализирует шаблоны рекомендаций."""
|
| 298 |
-
return {}
|
| 299 |
-
|
| 300 |
-
def _generate_no_anomalies_report(self) -> str:
|
| 301 |
-
"""Генерирует отчёт, когда аномалий не обнаружено."""
|
| 302 |
-
return """# Анализ первопричин и рекомендации
|
| 303 |
-
|
| 304 |
-
## Результаты анализа
|
| 305 |
-
|
| 306 |
-
**Обнаружено аномалий:** 0
|
| 307 |
-
|
| 308 |
-
✅ **Система работает стабильно.** В логах не обнаружено значительных аномалий или паттернов, указывающих на проблемы.
|
| 309 |
-
|
| 310 |
-
### Общие рекомендации
|
| 311 |
-
|
| 312 |
-
- Продолжайте регулярный мониторинг логов
|
| 313 |
-
- Поддерживайте текущий уровень логирования
|
| 314 |
-
- Настройте автоматические проверки для раннего обнаружения проблем
|
| 315 |
-
- Регулярно просматривайте метрики производительности
|
| 316 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
schemas/__pycache__/schemas.cpython-314.pyc
CHANGED
|
Binary files a/schemas/__pycache__/schemas.cpython-314.pyc and b/schemas/__pycache__/schemas.cpython-314.pyc differ
|
|
|