Spaces:
Sleeping
Sleeping
Commit
·
d76ef9a
1
Parent(s):
754c34e
add
Browse files- README.md +16 -10
- app.py +138 -43
- app.pyi +246 -0
- pipeline.py +75 -41
- preprocess.py +15 -0
- requirements.txt +1 -1
- retrieval.py +15 -0
README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: "4.44.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
#
|
| 13 |
|
| 14 |
-
Gradio
|
| 15 |
|
| 16 |
## Установка локально
|
| 17 |
|
|
@@ -25,7 +25,13 @@ pip install -r requirements.txt
|
|
| 25 |
python app.py
|
| 26 |
```
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
## Запуск на Hugging Face Spaces
|
| 31 |
|
|
@@ -35,15 +41,15 @@ python app.py
|
|
| 35 |
|
| 36 |
## Использование
|
| 37 |
|
| 38 |
-
- Вставьте логи/стектрейс, выберите
|
| 39 |
-
- Опции:
|
| 40 |
-
- Нажмите
|
| 41 |
|
| 42 |
## Примеры
|
| 43 |
|
| 44 |
- `samples/sample_python.txt` — HTTP timeout.
|
| 45 |
-
- `samples/sample_k8s.txt` — CrashLoop/OOMKilled
|
| 46 |
-
- `samples/sample_java.txt` — NullPointerException auth failure.
|
| 47 |
|
| 48 |
## Состав
|
| 49 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Анализатор логов
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.1"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Анализатор логов
|
| 13 |
|
| 14 |
+
Gradio‑демо: вставляете логи/стектрейс, получаете тип инцидента, человеческое объяснение, вероятную причину, набор проверок и локальные ранбуки. Пайплайн использует трансформеры (zero-shot классификатор, summarizer, sentence-embedding retriever, при желании reranker и NLI).
|
| 15 |
|
| 16 |
## Установка локально
|
| 17 |
|
|
|
|
| 25 |
python app.py
|
| 26 |
```
|
| 27 |
|
| 28 |
+
По умолчанию приложение слушает `127.0.0.1:7860` без публичного шаринга. Чтобы открыть интерфейс наружу, задайте:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
GRADIO_HOST=0.0.0.0 GRADIO_SHARE=1 python app.py
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
или выставьте нужный `GRADIO_SERVER_NAME` / `PORT`.
|
| 35 |
|
| 36 |
## Запуск на Hugging Face Spaces
|
| 37 |
|
|
|
|
| 41 |
|
| 42 |
## Использование
|
| 43 |
|
| 44 |
+
- Вставьте логи/стектрейс, выберите источник (auto/python/java/node/k8s).
|
| 45 |
+
- Опции: поиск по локальной базе (`kb/`), NLI-проверка гипотез, слайдер детализации объяснения.
|
| 46 |
+
- Нажмите **Анализировать**: вкладки покажут тип инцидента, пояснение, причину/проверки, найденные ранбуки, проверку гипотез и шаблон тикета. Можно выгрузить JSON.
|
| 47 |
|
| 48 |
## Примеры
|
| 49 |
|
| 50 |
- `samples/sample_python.txt` — HTTP timeout.
|
| 51 |
+
- `samples/sample_k8s.txt` — под с CrashLoop/OOMKilled.
|
| 52 |
+
- `samples/sample_java.txt` — NullPointerException из-за auth failure.
|
| 53 |
|
| 54 |
## Состав
|
| 55 |
|
app.py
CHANGED
|
@@ -9,25 +9,107 @@ from pipeline import IncidentPipeline, IncidentResult, serialize_result
|
|
| 9 |
from preprocess import truncate_logs
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
pipeline = IncidentPipeline()
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def format_incident_section(result: IncidentResult) -> str:
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
return (
|
| 19 |
-
f"
|
| 20 |
-
f"
|
| 21 |
-
f"
|
| 22 |
)
|
| 23 |
|
| 24 |
|
| 25 |
def format_cause_section(result: IncidentResult) -> str:
|
|
|
|
|
|
|
|
|
|
| 26 |
checks_md = "\n".join([f"- {c}" for c in result.checks])
|
| 27 |
-
return f"
|
| 28 |
|
| 29 |
|
| 30 |
def analyze_logs(logs: str, source: str, use_retrieval: bool, use_nli: bool, verbosity: int):
|
|
|
|
|
|
|
|
|
|
| 31 |
try:
|
| 32 |
res = pipeline.process(
|
| 33 |
logs,
|
|
@@ -37,7 +119,7 @@ def analyze_logs(logs: str, source: str, use_retrieval: bool, use_nli: bool, ver
|
|
| 37 |
verbosity=verbosity,
|
| 38 |
)
|
| 39 |
except Exception as exc:
|
| 40 |
-
message = f"
|
| 41 |
empty_table: List[List[Any]] = []
|
| 42 |
return (
|
| 43 |
message,
|
|
@@ -46,7 +128,7 @@ def analyze_logs(logs: str, source: str, use_retrieval: bool, use_nli: bool, ver
|
|
| 46 |
empty_table,
|
| 47 |
empty_table,
|
| 48 |
None,
|
| 49 |
-
f"
|
| 50 |
)
|
| 51 |
|
| 52 |
retrieval_rows = [
|
|
@@ -64,33 +146,39 @@ def analyze_logs(logs: str, source: str, use_retrieval: bool, use_nli: bool, ver
|
|
| 64 |
retrieval_rows,
|
| 65 |
verification_rows,
|
| 66 |
state_payload,
|
| 67 |
-
"
|
| 68 |
)
|
| 69 |
|
| 70 |
|
| 71 |
def ticket_template(state: Optional[str], logs: str) -> str:
|
|
|
|
|
|
|
|
|
|
| 72 |
if not state:
|
| 73 |
-
return "
|
| 74 |
try:
|
| 75 |
parsed = json.loads(state) if isinstance(state, str) else state
|
| 76 |
except Exception:
|
| 77 |
-
return "
|
| 78 |
clipped_logs = truncate_logs(logs, head_lines=30, tail_lines=10, max_lines=60)
|
| 79 |
checks = parsed.get("checks") or []
|
| 80 |
checks_md = "\n".join(f"- {c}" for c in checks)
|
| 81 |
-
summary = f"{parsed.get('incident_label','?')} — {parsed.get('explanation','')[:180]}"
|
| 82 |
template = (
|
| 83 |
-
f"
|
| 84 |
-
f"
|
| 85 |
-
f"
|
| 86 |
-
f"
|
| 87 |
-
f"
|
| 88 |
-
f"
|
| 89 |
)
|
| 90 |
return template
|
| 91 |
|
| 92 |
|
| 93 |
def export_json(state: Optional[str]):
|
|
|
|
|
|
|
|
|
|
| 94 |
if not state:
|
| 95 |
return None
|
| 96 |
# If state is dict, dump; if already JSON string, use as-is.
|
|
@@ -102,47 +190,47 @@ def export_json(state: Optional[str]):
|
|
| 102 |
return tmp.name
|
| 103 |
|
| 104 |
|
| 105 |
-
with gr.Blocks(title="
|
| 106 |
-
gr.Markdown("#
|
| 107 |
-
#
|
| 108 |
state_box = gr.Textbox(visible=False, show_label=False)
|
| 109 |
|
| 110 |
with gr.Row():
|
| 111 |
-
with gr.Column(scale=
|
| 112 |
-
logs_input = gr.Textbox(lines=20, label="
|
| 113 |
source_dropdown = gr.Dropdown(
|
| 114 |
["auto", "python", "java", "node", "k8s"],
|
| 115 |
value="auto",
|
| 116 |
-
label="
|
| 117 |
)
|
| 118 |
-
use_retrieval = gr.Checkbox(value=True, label="
|
| 119 |
-
use_nli = gr.Checkbox(value=False, label="
|
| 120 |
-
verbosity_slider = gr.Slider(0, 2, value=1, step=1, label="
|
| 121 |
-
analyze_btn = gr.Button("
|
| 122 |
-
ticket_btn = gr.Button("
|
| 123 |
-
export_btn = gr.Button("
|
| 124 |
-
json_output =
|
| 125 |
-
status = gr.Markdown("
|
| 126 |
-
with gr.Column(scale=
|
| 127 |
-
with gr.Tab("
|
| 128 |
incident_md = gr.Markdown()
|
| 129 |
-
with gr.Tab("
|
| 130 |
explanation_md = gr.Markdown()
|
| 131 |
-
with gr.Tab("
|
| 132 |
cause_md = gr.Markdown()
|
| 133 |
-
with gr.Tab("
|
| 134 |
retrieval_df = gr.Dataframe(
|
| 135 |
-
headers=["
|
| 136 |
datatype=["str", "number", "str", "str"],
|
| 137 |
interactive=False,
|
| 138 |
)
|
| 139 |
-
with gr.Tab("
|
| 140 |
verification_df = gr.Dataframe(
|
| 141 |
-
headers=["
|
| 142 |
datatype=["str", "str", "number"],
|
| 143 |
interactive=False,
|
| 144 |
)
|
| 145 |
-
with gr.Tab("
|
| 146 |
ticket_md = gr.Markdown()
|
| 147 |
|
| 148 |
analyze_btn.click(
|
|
@@ -165,6 +253,13 @@ with gr.Blocks(title="Log Compiler App") as demo:
|
|
| 165 |
|
| 166 |
|
| 167 |
if __name__ == "__main__":
|
| 168 |
-
share_env = os.getenv("GRADIO_SHARE")
|
| 169 |
in_hf_space = bool(os.getenv("SPACE_ID") or os.getenv("HF_SPACE"))
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from preprocess import truncate_logs
|
| 10 |
|
| 11 |
|
| 12 |
+
class DownloadOnlyFile(gr.File):
|
| 13 |
+
"""Файл только для скачивания, скрытый из OpenAPI-схемы Gradio."""
|
| 14 |
+
|
| 15 |
+
is_template = True
|
| 16 |
+
|
| 17 |
+
@property
|
| 18 |
+
def skip_api(self) -> bool:
|
| 19 |
+
return True
|
| 20 |
+
|
| 21 |
+
|
| 22 |
pipeline = IncidentPipeline()
|
| 23 |
|
| 24 |
|
| 25 |
+
LABEL_DISPLAY = {
|
| 26 |
+
"oom": "Переполнение памяти (OOM)",
|
| 27 |
+
"timeout": "Таймаут",
|
| 28 |
+
"auth_failure": "Ошибка аутентификации/авторизации",
|
| 29 |
+
"db_connection": "Сбой подключения к базе данных",
|
| 30 |
+
"dns_resolution": "Ошибка DNS",
|
| 31 |
+
"tls_handshake": "Ошибка TLS-рукопожатия",
|
| 32 |
+
"crashloop": "CrashLoop / повторные рестарты",
|
| 33 |
+
"null_pointer": "NullPointer / None reference",
|
| 34 |
+
"resource_exhaustion": "Исчерпание ресурсов",
|
| 35 |
+
"network_partition": "Сетевая изоляция",
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
SOURCE_DISPLAY = {
|
| 39 |
+
"python": "Python",
|
| 40 |
+
"java": "Java",
|
| 41 |
+
"node": "Node.js",
|
| 42 |
+
"k8s": "Kubernetes",
|
| 43 |
+
"auto": "Auto",
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
SIGNATURE_DISPLAY = {
|
| 47 |
+
"stacktrace": "стектрейс",
|
| 48 |
+
"timestamps": "таймстемпы",
|
| 49 |
+
"log_levels": "уровни логов",
|
| 50 |
+
"k8s": "ошибки Kubernetes",
|
| 51 |
+
"oom": "признаки OOM",
|
| 52 |
+
"timeout": "упоминания таймаута",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
SPEC_SUFFIX = "_specific"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def human_label(label: str) -> str:
|
| 59 |
+
"""
|
| 60 |
+
Возвращает человеко-понятное название категории инцидента.
|
| 61 |
+
"""
|
| 62 |
+
if label.endswith(SPEC_SUFFIX):
|
| 63 |
+
base = label[: -len(SPEC_SUFFIX)]
|
| 64 |
+
source_name = SOURCE_DISPLAY.get(base, base)
|
| 65 |
+
return f"Категория, специфичная для {source_name}"
|
| 66 |
+
return LABEL_DISPLAY.get(label, label)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def human_signature(sig: str) -> str:
|
| 70 |
+
"""
|
| 71 |
+
Конвертирует машинную сигнатуру в более дружелюбный текст.
|
| 72 |
+
"""
|
| 73 |
+
return SIGNATURE_DISPLAY.get(sig, sig)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def env_flag(name: str, default: bool = False) -> bool:
|
| 77 |
+
"""
|
| 78 |
+
Безопасно читает булевы переменные окружения (1/0, true/false и т.д.).
|
| 79 |
+
"""
|
| 80 |
+
raw = os.getenv(name)
|
| 81 |
+
if raw is None:
|
| 82 |
+
return default
|
| 83 |
+
return raw.lower() in ("1", "true", "yes", "on")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
def format_incident_section(result: IncidentResult) -> str:
|
| 87 |
+
"""
|
| 88 |
+
Формирует markdown-блок с типом инцидента, альтернативами и сигнатурами.
|
| 89 |
+
"""
|
| 90 |
+
alt_text = ", ".join(
|
| 91 |
+
f"{human_label(a['label'])} ({a['score']:.2f})" for a in result.incident_alternatives
|
| 92 |
+
)
|
| 93 |
+
sigs = ", ".join(human_signature(sig) for sig in result.signatures) if result.signatures else "нет"
|
| 94 |
return (
|
| 95 |
+
f"**Инцидент:** {human_label(result.incident_label)} (уверенность {result.incident_score:.2f})\n\n"
|
| 96 |
+
f"**Альтернативы:** {alt_text if alt_text else 'н/д'}\n\n"
|
| 97 |
+
f"**Обнаруженные сигнатуры:** {sigs}"
|
| 98 |
)
|
| 99 |
|
| 100 |
|
| 101 |
def format_cause_section(result: IncidentResult) -> str:
|
| 102 |
+
"""
|
| 103 |
+
Создаёт markdown с причиной и списком проверок.
|
| 104 |
+
"""
|
| 105 |
checks_md = "\n".join([f"- {c}" for c in result.checks])
|
| 106 |
+
return f"**Вероятная причина:** {result.likely_cause}\n\n**Проверки / следующие шаги:**\n{checks_md}"
|
| 107 |
|
| 108 |
|
| 109 |
def analyze_logs(logs: str, source: str, use_retrieval: bool, use_nli: bool, verbosity: int):
|
| 110 |
+
"""
|
| 111 |
+
Основная функция кнопки «Анализировать»: прогоняет пайплайн и возвращает выводы.
|
| 112 |
+
"""
|
| 113 |
try:
|
| 114 |
res = pipeline.process(
|
| 115 |
logs,
|
|
|
|
| 119 |
verbosity=verbosity,
|
| 120 |
)
|
| 121 |
except Exception as exc:
|
| 122 |
+
message = f"Ошибка: {exc}"
|
| 123 |
empty_table: List[List[Any]] = []
|
| 124 |
return (
|
| 125 |
message,
|
|
|
|
| 128 |
empty_table,
|
| 129 |
empty_table,
|
| 130 |
None,
|
| 131 |
+
f"Сбой: {exc}",
|
| 132 |
)
|
| 133 |
|
| 134 |
retrieval_rows = [
|
|
|
|
| 146 |
retrieval_rows,
|
| 147 |
verification_rows,
|
| 148 |
state_payload,
|
| 149 |
+
"Анализ завершён.",
|
| 150 |
)
|
| 151 |
|
| 152 |
|
| 153 |
def ticket_template(state: Optional[str], logs: str) -> str:
|
| 154 |
+
"""
|
| 155 |
+
Собирает черновик тикета опираясь на результаты последнего анализа.
|
| 156 |
+
"""
|
| 157 |
if not state:
|
| 158 |
+
return "Сначала запустите анализ."
|
| 159 |
try:
|
| 160 |
parsed = json.loads(state) if isinstance(state, str) else state
|
| 161 |
except Exception:
|
| 162 |
+
return "Состояние повреждено. Повторите анализ."
|
| 163 |
clipped_logs = truncate_logs(logs, head_lines=30, tail_lines=10, max_lines=60)
|
| 164 |
checks = parsed.get("checks") or []
|
| 165 |
checks_md = "\n".join(f"- {c}" for c in checks)
|
| 166 |
+
summary = f"{human_label(parsed.get('incident_label','?'))} — {parsed.get('explanation','')[:180]}"
|
| 167 |
template = (
|
| 168 |
+
f"Сводка:\n{summary}\n\n"
|
| 169 |
+
f"Шаги для воспроизведения:\n- Опишите последовательность, которая привела к сбою.\n- Приложите проблемный запрос или данные.\n\n"
|
| 170 |
+
f"Ожидаемый результат:\n- Сервис успешно обрабатывает запрос.\n\n"
|
| 171 |
+
f"Фактический результат:\n- {parsed.get('likely_cause','')}\n\n"
|
| 172 |
+
f"Проверки / дальнейшие шаги:\n{checks_md}\n\n"
|
| 173 |
+
f"Фрагмент логов:\n{clipped_logs}\n"
|
| 174 |
)
|
| 175 |
return template
|
| 176 |
|
| 177 |
|
| 178 |
def export_json(state: Optional[str]):
|
| 179 |
+
"""
|
| 180 |
+
Сохраняет результат анализа во временный JSON и возвращает путь до него.
|
| 181 |
+
"""
|
| 182 |
if not state:
|
| 183 |
return None
|
| 184 |
# If state is dict, dump; if already JSON string, use as-is.
|
|
|
|
| 190 |
return tmp.name
|
| 191 |
|
| 192 |
|
| 193 |
+
with gr.Blocks(title="Анализатор логов") as demo:
|
| 194 |
+
gr.Markdown("# Анализатор логов\nВставьте логи/стектрейс и получите тип инцидента, объяснения и подсказки по расследованию.")
|
| 195 |
+
# Скрытое поле для сериализованного состояния.
|
| 196 |
state_box = gr.Textbox(visible=False, show_label=False)
|
| 197 |
|
| 198 |
with gr.Row():
|
| 199 |
+
with gr.Column(scale=5):
|
| 200 |
+
logs_input = gr.Textbox(lines=20, label="Логи / стек", placeholder="Вставьте логи сюда...")
|
| 201 |
source_dropdown = gr.Dropdown(
|
| 202 |
["auto", "python", "java", "node", "k8s"],
|
| 203 |
value="auto",
|
| 204 |
+
label="Источник",
|
| 205 |
)
|
| 206 |
+
use_retrieval = gr.Checkbox(value=True, label="Использовать поиск по базе знаний")
|
| 207 |
+
use_nli = gr.Checkbox(value=False, label="Проверять гипотезы (NLI)")
|
| 208 |
+
verbosity_slider = gr.Slider(0, 2, value=1, step=1, label="Детализация объяснения")
|
| 209 |
+
analyze_btn = gr.Button("Анализировать")
|
| 210 |
+
ticket_btn = gr.Button("Сформировать шаблон тикета")
|
| 211 |
+
export_btn = gr.Button("Экспорт JSON")
|
| 212 |
+
json_output = DownloadOnlyFile(label="Экспорт JSON")
|
| 213 |
+
status = gr.Markdown("Готово.")
|
| 214 |
+
with gr.Column(scale=6):
|
| 215 |
+
with gr.Tab("Тип инцидента"):
|
| 216 |
incident_md = gr.Markdown()
|
| 217 |
+
with gr.Tab("Пояснение"):
|
| 218 |
explanation_md = gr.Markdown()
|
| 219 |
+
with gr.Tab("Причина и проверки"):
|
| 220 |
cause_md = gr.Markdown()
|
| 221 |
+
with gr.Tab("Найденные ранбуки"):
|
| 222 |
retrieval_df = gr.Dataframe(
|
| 223 |
+
headers=["Название", "Сходство", "Путь", "Фрагмент"],
|
| 224 |
datatype=["str", "number", "str", "str"],
|
| 225 |
interactive=False,
|
| 226 |
)
|
| 227 |
+
with gr.Tab("Проверка гипотез"):
|
| 228 |
verification_df = gr.Dataframe(
|
| 229 |
+
headers=["Гипотеза", "Метка", "Счёт"],
|
| 230 |
datatype=["str", "str", "number"],
|
| 231 |
interactive=False,
|
| 232 |
)
|
| 233 |
+
with gr.Tab("Шаблон тикета"):
|
| 234 |
ticket_md = gr.Markdown()
|
| 235 |
|
| 236 |
analyze_btn.click(
|
|
|
|
| 253 |
|
| 254 |
|
| 255 |
if __name__ == "__main__":
|
|
|
|
| 256 |
in_hf_space = bool(os.getenv("SPACE_ID") or os.getenv("HF_SPACE"))
|
| 257 |
+
share_flag = False if in_hf_space else env_flag("GRADIO_SHARE", default=False)
|
| 258 |
+
host = os.getenv("GRADIO_HOST") or os.getenv("GRADIO_SERVER_NAME") or "127.0.0.1"
|
| 259 |
+
port = int(os.getenv("PORT") or os.getenv("GRADIO_SERVER_PORT") or 7860)
|
| 260 |
+
demo.queue(api_open=False).launch(
|
| 261 |
+
server_name=host,
|
| 262 |
+
server_port=port,
|
| 263 |
+
share=share_flag,
|
| 264 |
+
show_api=False,
|
| 265 |
+
)
|
app.pyi
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import tempfile
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
from pipeline import IncidentPipeline, IncidentResult, serialize_result
|
| 9 |
+
from preprocess import truncate_logs
|
| 10 |
+
|
| 11 |
+
from gradio.events import Dependency
|
| 12 |
+
|
| 13 |
+
class DownloadOnlyFile(gr.File):
|
| 14 |
+
"""Файл только для скачивания, скрытый из OpenAPI-схемы Gradio."""
|
| 15 |
+
|
| 16 |
+
is_template = True
|
| 17 |
+
|
| 18 |
+
@property
|
| 19 |
+
def skip_api(self) -> bool:
|
| 20 |
+
return True
|
| 21 |
+
from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING
|
| 22 |
+
from gradio.blocks import Block
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from gradio.components import Timer
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
pipeline = IncidentPipeline()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
LABEL_DISPLAY = {
|
| 31 |
+
"oom": "Переполнение памяти (OOM)",
|
| 32 |
+
"timeout": "Таймаут",
|
| 33 |
+
"auth_failure": "Ошибка аутентификации/авторизации",
|
| 34 |
+
"db_connection": "Сбой подключения к базе данных",
|
| 35 |
+
"dns_resolution": "Ошибка DNS",
|
| 36 |
+
"tls_handshake": "Ошибка TLS-рукопожатия",
|
| 37 |
+
"crashloop": "CrashLoop / повторные рестарты",
|
| 38 |
+
"null_pointer": "NullPointer / None reference",
|
| 39 |
+
"resource_exhaustion": "Исчерпание ресурсов",
|
| 40 |
+
"network_partition": "Сетевая изоляция",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
SOURCE_DISPLAY = {
|
| 44 |
+
"python": "Python",
|
| 45 |
+
"java": "Java",
|
| 46 |
+
"node": "Node.js",
|
| 47 |
+
"k8s": "Kubernetes",
|
| 48 |
+
"auto": "Auto",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
SIGNATURE_DISPLAY = {
|
| 52 |
+
"stacktrace": "стектрейс",
|
| 53 |
+
"timestamps": "таймстемпы",
|
| 54 |
+
"log_levels": "уровни логов",
|
| 55 |
+
"k8s": "ошибки Kubernetes",
|
| 56 |
+
"oom": "признаки OOM",
|
| 57 |
+
"timeout": "упоминания таймаута",
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
SPEC_SUFFIX = "_specific"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def human_label(label: str) -> str:
|
| 64 |
+
if label.endswith(SPEC_SUFFIX):
|
| 65 |
+
base = label[: -len(SPEC_SUFFIX)]
|
| 66 |
+
source_name = SOURCE_DISPLAY.get(base, base)
|
| 67 |
+
return f"Категория, специфичная для {source_name}"
|
| 68 |
+
return LABEL_DISPLAY.get(label, label)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def human_signature(sig: str) -> str:
|
| 72 |
+
return SIGNATURE_DISPLAY.get(sig, sig)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def env_flag(name: str, default: bool = False) -> bool:
|
| 76 |
+
raw = os.getenv(name)
|
| 77 |
+
if raw is None:
|
| 78 |
+
return default
|
| 79 |
+
return raw.lower() in ("1", "true", "yes", "on")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def format_incident_section(result: IncidentResult) -> str:
|
| 83 |
+
alt_text = ", ".join(
|
| 84 |
+
f"{human_label(a['label'])} ({a['score']:.2f})" for a in result.incident_alternatives
|
| 85 |
+
)
|
| 86 |
+
sigs = ", ".join(human_signature(sig) for sig in result.signatures) if result.signatures else "нет"
|
| 87 |
+
return (
|
| 88 |
+
f"**Инцидент:** {human_label(result.incident_label)} (уверенность {result.incident_score:.2f})\n\n"
|
| 89 |
+
f"**Альтернативы:** {alt_text if alt_text else 'н/д'}\n\n"
|
| 90 |
+
f"**Обнаруженные сигнатуры:** {sigs}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def format_cause_section(result: IncidentResult) -> str:
|
| 95 |
+
checks_md = "\n".join([f"- {c}" for c in result.checks])
|
| 96 |
+
return f"**Вероятная причина:** {result.likely_cause}\n\n**Проверки / следующие шаги:**\n{checks_md}"
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def analyze_logs(logs: str, source: str, use_retrieval: bool, use_nli: bool, verbosity: int):
|
| 100 |
+
try:
|
| 101 |
+
res = pipeline.process(
|
| 102 |
+
logs,
|
| 103 |
+
source=source,
|
| 104 |
+
use_retrieval=use_retrieval,
|
| 105 |
+
use_nli=use_nli,
|
| 106 |
+
verbosity=verbosity,
|
| 107 |
+
)
|
| 108 |
+
except Exception as exc:
|
| 109 |
+
message = f"Ошибка: {exc}"
|
| 110 |
+
empty_table: List[List[Any]] = []
|
| 111 |
+
return (
|
| 112 |
+
message,
|
| 113 |
+
"",
|
| 114 |
+
"",
|
| 115 |
+
empty_table,
|
| 116 |
+
empty_table,
|
| 117 |
+
None,
|
| 118 |
+
f"Сбой: {exc}",
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
retrieval_rows = [
|
| 122 |
+
[r["title"], round(r["score"], 3), r["path"], r["excerpt"]]
|
| 123 |
+
for r in res.retrieved
|
| 124 |
+
]
|
| 125 |
+
verification_rows = [
|
| 126 |
+
[v["hypothesis"], v["label"], round(v["score"], 3)] for v in res.verification
|
| 127 |
+
]
|
| 128 |
+
state_payload = serialize_result(res)
|
| 129 |
+
return (
|
| 130 |
+
format_incident_section(res),
|
| 131 |
+
res.explanation,
|
| 132 |
+
format_cause_section(res),
|
| 133 |
+
retrieval_rows,
|
| 134 |
+
verification_rows,
|
| 135 |
+
state_payload,
|
| 136 |
+
"Анализ завершён.",
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def ticket_template(state: Optional[str], logs: str) -> str:
|
| 141 |
+
if not state:
|
| 142 |
+
return "Сначала запустите анализ."
|
| 143 |
+
try:
|
| 144 |
+
parsed = json.loads(state) if isinstance(state, str) else state
|
| 145 |
+
except Exception:
|
| 146 |
+
return "Состояние повреждено. Повторите анализ."
|
| 147 |
+
clipped_logs = truncate_logs(logs, head_lines=30, tail_lines=10, max_lines=60)
|
| 148 |
+
checks = parsed.get("checks") or []
|
| 149 |
+
checks_md = "\n".join(f"- {c}" for c in checks)
|
| 150 |
+
summary = f"{human_label(parsed.get('incident_label','?'))} — {parsed.get('explanation','')[:180]}"
|
| 151 |
+
template = (
|
| 152 |
+
f"Сводка:\n{summary}\n\n"
|
| 153 |
+
f"Шаги для воспроизведения:\n- Опишите последовательность, которая привела к сбою.\n- Приложите проблемный запрос или данные.\n\n"
|
| 154 |
+
f"Ожидаемый результат:\n- Сервис успешно обрабатывает запрос.\n\n"
|
| 155 |
+
f"Фактический результат:\n- {parsed.get('likely_cause','')}\n\n"
|
| 156 |
+
f"Проверки / дальнейшие шаги:\n{checks_md}\n\n"
|
| 157 |
+
f"Фрагмент логов:\n{clipped_logs}\n"
|
| 158 |
+
)
|
| 159 |
+
return template
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def export_json(state: Optional[str]):
|
| 163 |
+
if not state:
|
| 164 |
+
return None
|
| 165 |
+
# If state is dict, dump; if already JSON string, use as-is.
|
| 166 |
+
data = json.dumps(state, ensure_ascii=False, indent=2) if isinstance(state, dict) else state
|
| 167 |
+
tmp = tempfile.NamedTemporaryFile("w", delete=False, suffix=".json", encoding="utf-8")
|
| 168 |
+
tmp.write(data)
|
| 169 |
+
tmp.flush()
|
| 170 |
+
tmp.close()
|
| 171 |
+
return tmp.name
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
with gr.Blocks(title="Анализатор логов") as demo:
|
| 175 |
+
gr.Markdown("# Анализатор логов\nВставьте логи/стектрейс и получите тип инцидента, объяснения и подсказки по расследованию.")
|
| 176 |
+
# Скрытое поле для сериализованного состояния.
|
| 177 |
+
state_box = gr.Textbox(visible=False, show_label=False)
|
| 178 |
+
|
| 179 |
+
with gr.Row():
|
| 180 |
+
with gr.Column(scale=5):
|
| 181 |
+
logs_input = gr.Textbox(lines=20, label="Логи / стек", placeholder="Вставьте логи сюда...")
|
| 182 |
+
source_dropdown = gr.Dropdown(
|
| 183 |
+
["auto", "python", "java", "node", "k8s"],
|
| 184 |
+
value="auto",
|
| 185 |
+
label="Источник",
|
| 186 |
+
)
|
| 187 |
+
use_retrieval = gr.Checkbox(value=True, label="Использовать поиск по базе знаний")
|
| 188 |
+
use_nli = gr.Checkbox(value=False, label="Проверять гипотезы (NLI)")
|
| 189 |
+
verbosity_slider = gr.Slider(0, 2, value=1, step=1, label="Детализация объяснения")
|
| 190 |
+
analyze_btn = gr.Button("Анализировать")
|
| 191 |
+
ticket_btn = gr.Button("Сформировать шаблон тикета")
|
| 192 |
+
export_btn = gr.Button("Экспорт JSON")
|
| 193 |
+
json_output = DownloadOnlyFile(label="Экспорт JSON")
|
| 194 |
+
status = gr.Markdown("Готово.")
|
| 195 |
+
with gr.Column(scale=6):
|
| 196 |
+
with gr.Tab("Тип инцидента"):
|
| 197 |
+
incident_md = gr.Markdown()
|
| 198 |
+
with gr.Tab("Пояснение"):
|
| 199 |
+
explanation_md = gr.Markdown()
|
| 200 |
+
with gr.Tab("Причина и проверки"):
|
| 201 |
+
cause_md = gr.Markdown()
|
| 202 |
+
with gr.Tab("Найденные ранбуки"):
|
| 203 |
+
retrieval_df = gr.Dataframe(
|
| 204 |
+
headers=["Название", "Сходство", "Путь", "Фрагмент"],
|
| 205 |
+
datatype=["str", "number", "str", "str"],
|
| 206 |
+
interactive=False,
|
| 207 |
+
)
|
| 208 |
+
with gr.Tab("Проверка гипотез"):
|
| 209 |
+
verification_df = gr.Dataframe(
|
| 210 |
+
headers=["Гипотеза", "Метка", "Счёт"],
|
| 211 |
+
datatype=["str", "str", "number"],
|
| 212 |
+
interactive=False,
|
| 213 |
+
)
|
| 214 |
+
with gr.Tab("Шаблон тикета"):
|
| 215 |
+
ticket_md = gr.Markdown()
|
| 216 |
+
|
| 217 |
+
analyze_btn.click(
|
| 218 |
+
fn=analyze_logs,
|
| 219 |
+
inputs=[logs_input, source_dropdown, use_retrieval, use_nli, verbosity_slider],
|
| 220 |
+
outputs=[incident_md, explanation_md, cause_md, retrieval_df, verification_df, state_box, status],
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
ticket_btn.click(
|
| 224 |
+
fn=ticket_template,
|
| 225 |
+
inputs=[state_box, logs_input],
|
| 226 |
+
outputs=ticket_md,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
export_btn.click(
|
| 230 |
+
fn=export_json,
|
| 231 |
+
inputs=state_box,
|
| 232 |
+
outputs=json_output,
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
if __name__ == "__main__":
|
| 237 |
+
in_hf_space = bool(os.getenv("SPACE_ID") or os.getenv("HF_SPACE"))
|
| 238 |
+
share_flag = False if in_hf_space else env_flag("GRADIO_SHARE", default=False)
|
| 239 |
+
host = os.getenv("GRADIO_HOST") or os.getenv("GRADIO_SERVER_NAME") or "127.0.0.1"
|
| 240 |
+
port = int(os.getenv("PORT") or os.getenv("GRADIO_SERVER_PORT") or 7860)
|
| 241 |
+
demo.queue(api_open=False).launch(
|
| 242 |
+
server_name=host,
|
| 243 |
+
server_port=port,
|
| 244 |
+
share=share_flag,
|
| 245 |
+
show_api=False,
|
| 246 |
+
)
|
pipeline.py
CHANGED
|
@@ -28,6 +28,9 @@ CANDIDATE_LABELS = [
|
|
| 28 |
|
| 29 |
@dataclass
|
| 30 |
class IncidentResult:
|
|
|
|
|
|
|
|
|
|
| 31 |
incident_label: str
|
| 32 |
incident_score: float
|
| 33 |
incident_alternatives: List[Dict]
|
|
@@ -40,7 +43,13 @@ class IncidentResult:
|
|
| 40 |
|
| 41 |
|
| 42 |
class ModelStore:
|
|
|
|
|
|
|
|
|
|
| 43 |
def __init__(self):
|
|
|
|
|
|
|
|
|
|
| 44 |
self.classifier = pipeline(
|
| 45 |
"zero-shot-classification",
|
| 46 |
model="facebook/bart-large-mnli",
|
|
@@ -59,11 +68,20 @@ class ModelStore:
|
|
| 59 |
|
| 60 |
|
| 61 |
class IncidentPipeline:
|
|
|
|
|
|
|
|
|
|
| 62 |
def __init__(self):
|
|
|
|
|
|
|
|
|
|
| 63 |
self.models = ModelStore()
|
| 64 |
self.retriever = RunbookRetriever()
|
| 65 |
|
| 66 |
def classify(self, text: str, source: str) -> Dict:
|
|
|
|
|
|
|
|
|
|
| 67 |
labels = list(CANDIDATE_LABELS)
|
| 68 |
if source and source != "auto":
|
| 69 |
labels.append(f"{source}_specific")
|
|
@@ -77,6 +95,9 @@ class IncidentPipeline:
|
|
| 77 |
return {"label": label, "score": score, "alternatives": alternatives}
|
| 78 |
|
| 79 |
def explain(self, text: str, verbosity: int = 1) -> str:
|
|
|
|
|
|
|
|
|
|
| 80 |
max_len = 180 + 60 * verbosity
|
| 81 |
min_len = 40 + 20 * verbosity
|
| 82 |
summary = self.models.summarizer(
|
|
@@ -90,78 +111,85 @@ class IncidentPipeline:
|
|
| 90 |
def generate_cause_and_checks(
|
| 91 |
self, result: PreprocessResult, label: str, retrieved: List[Dict]
|
| 92 |
) -> tuple[str, List[str]]:
|
|
|
|
|
|
|
|
|
|
| 93 |
cause_map = {
|
| 94 |
-
"oom": "
|
| 95 |
-
"crashloop": "
|
| 96 |
-
"timeout": "
|
| 97 |
-
"auth_failure": "
|
| 98 |
-
"db_connection": "
|
| 99 |
-
"dns_resolution": "
|
| 100 |
-
"tls_handshake": "TLS
|
| 101 |
-
"null_pointer": "
|
| 102 |
-
"resource_exhaustion": "
|
| 103 |
-
"network_partition": "
|
| 104 |
}
|
| 105 |
-
cause = cause_map.get(label, f"
|
| 106 |
checks: List[str] = [
|
| 107 |
-
"
|
| 108 |
-
"
|
| 109 |
-
"
|
| 110 |
]
|
| 111 |
if label == "oom" or "oom" in result.signatures:
|
| 112 |
checks += [
|
| 113 |
-
"
|
| 114 |
-
"
|
| 115 |
-
"
|
| 116 |
-
"
|
| 117 |
]
|
| 118 |
if label in ("timeout",):
|
| 119 |
checks += [
|
| 120 |
-
"
|
| 121 |
-
"
|
| 122 |
-
"
|
| 123 |
]
|
| 124 |
if label in ("auth_failure",):
|
| 125 |
checks += [
|
| 126 |
-
"
|
| 127 |
-
"
|
| 128 |
-
"
|
| 129 |
]
|
| 130 |
if label in ("db_connection",):
|
| 131 |
checks += [
|
| 132 |
-
"
|
| 133 |
-
"
|
| 134 |
-
"
|
| 135 |
]
|
| 136 |
if label in ("dns_resolution",):
|
| 137 |
checks += [
|
| 138 |
-
"
|
| 139 |
-
"
|
| 140 |
-
"
|
| 141 |
]
|
| 142 |
if label in ("tls_handshake",):
|
| 143 |
checks += [
|
| 144 |
-
"
|
| 145 |
-
"
|
| 146 |
-
"
|
| 147 |
]
|
| 148 |
if label in ("crashloop",):
|
| 149 |
checks += [
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"
|
| 153 |
]
|
| 154 |
if retrieved:
|
| 155 |
-
checks.append(f"
|
| 156 |
# Ensure at least 5 checks
|
| 157 |
while len(checks) < 5:
|
| 158 |
-
checks.append("
|
| 159 |
return cause, checks[:10]
|
| 160 |
|
| 161 |
def verify_hypotheses(self, premise: str, hypotheses: List[str]) -> List[Dict]:
|
|
|
|
|
|
|
|
|
|
| 162 |
results = []
|
| 163 |
for hyp in hypotheses:
|
| 164 |
-
|
|
|
|
| 165 |
results.append({"hypothesis": hyp, "label": pred["label"], "score": float(pred["score"])})
|
| 166 |
return results
|
| 167 |
|
|
@@ -173,8 +201,11 @@ class IncidentPipeline:
|
|
| 173 |
use_nli: bool = False,
|
| 174 |
verbosity: int = 1,
|
| 175 |
) -> IncidentResult:
|
|
|
|
|
|
|
|
|
|
| 176 |
if not raw_text or not raw_text.strip():
|
| 177 |
-
raise ValueError("
|
| 178 |
pre = preprocess_logs(raw_text)
|
| 179 |
cls = self.classify(pre.cleaned_text, source)
|
| 180 |
explanation = self.explain(pre.cleaned_text, verbosity=verbosity)
|
|
@@ -182,7 +213,7 @@ class IncidentPipeline:
|
|
| 182 |
cause, checks = self.generate_cause_and_checks(pre, cls["label"], retrieved)
|
| 183 |
verification = []
|
| 184 |
if use_nli:
|
| 185 |
-
hypotheses = [cause] + [f"
|
| 186 |
verification = self.verify_hypotheses(pre.cleaned_text, hypotheses)
|
| 187 |
return IncidentResult(
|
| 188 |
incident_label=cls["label"],
|
|
@@ -198,4 +229,7 @@ class IncidentPipeline:
|
|
| 198 |
|
| 199 |
|
| 200 |
def serialize_result(result: IncidentResult) -> str:
|
|
|
|
|
|
|
|
|
|
| 201 |
return json.dumps(asdict(result), indent=2, ensure_ascii=False)
|
|
|
|
| 28 |
|
| 29 |
@dataclass
|
| 30 |
class IncidentResult:
|
| 31 |
+
"""
|
| 32 |
+
Контейнер с результатом пайплайна по одному запуску.
|
| 33 |
+
"""
|
| 34 |
incident_label: str
|
| 35 |
incident_score: float
|
| 36 |
incident_alternatives: List[Dict]
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
class ModelStore:
|
| 46 |
+
"""
|
| 47 |
+
Хранит и переиспользует все необходимые ML-пайплайны.
|
| 48 |
+
"""
|
| 49 |
def __init__(self):
|
| 50 |
+
"""
|
| 51 |
+
Загружает и кэширует все необходимые трансформерные пайплайны.
|
| 52 |
+
"""
|
| 53 |
self.classifier = pipeline(
|
| 54 |
"zero-shot-classification",
|
| 55 |
model="facebook/bart-large-mnli",
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
class IncidentPipeline:
|
| 71 |
+
"""
|
| 72 |
+
Компонуёт все стадии анализа логов в единый процесс.
|
| 73 |
+
"""
|
| 74 |
def __init__(self):
|
| 75 |
+
"""
|
| 76 |
+
Собирает модели и ретривер, готовые к переиспользованию.
|
| 77 |
+
"""
|
| 78 |
self.models = ModelStore()
|
| 79 |
self.retriever = RunbookRetriever()
|
| 80 |
|
| 81 |
def classify(self, text: str, source: str) -> Dict:
|
| 82 |
+
"""
|
| 83 |
+
Определяет тип инцидента zero-shot классификатором.
|
| 84 |
+
"""
|
| 85 |
labels = list(CANDIDATE_LABELS)
|
| 86 |
if source and source != "auto":
|
| 87 |
labels.append(f"{source}_specific")
|
|
|
|
| 95 |
return {"label": label, "score": score, "alternatives": alternatives}
|
| 96 |
|
| 97 |
def explain(self, text: str, verbosity: int = 1) -> str:
|
| 98 |
+
"""
|
| 99 |
+
Делает сжатое пояснение к логам при помощи summarizer.
|
| 100 |
+
"""
|
| 101 |
max_len = 180 + 60 * verbosity
|
| 102 |
min_len = 40 + 20 * verbosity
|
| 103 |
summary = self.models.summarizer(
|
|
|
|
| 111 |
def generate_cause_and_checks(
|
| 112 |
self, result: PreprocessResult, label: str, retrieved: List[Dict]
|
| 113 |
) -> tuple[str, List[str]]:
|
| 114 |
+
"""
|
| 115 |
+
Подбирает человеко-понятную причину и список проверок по категории.
|
| 116 |
+
"""
|
| 117 |
cause_map = {
|
| 118 |
+
"oom": "Сервис, вероятно, исчерпал память и был аварийно завершён.",
|
| 119 |
+
"crashloop": "Контейнер постоянно перезапускается из-за повторяющихся сбоев или неуспешных health-check.",
|
| 120 |
+
"timeout": "Верхний уровень или зависимость не ответили вовремя.",
|
| 121 |
+
"auth_failure": "Аутентификация/авторизация отклонена (истёкший токен, нехватка прав или неверная конфигурация).",
|
| 122 |
+
"db_connection": "Пул подключений к базе данных исчерпан либо соединение отвергнуто.",
|
| 123 |
+
"dns_resolution": "Не удалось разрешить DNS-имя целевого хоста.",
|
| 124 |
+
"tls_handshake": "TLS-рукопожатие завершилось ошибкой (сертификат, протокол или шифр).",
|
| 125 |
+
"null_pointer": "Приложение встретило null/None и аварийно завершилось.",
|
| 126 |
+
"resource_exhaustion": "Системные ресурсы (CPU/дескрипторы файлов) исчерпаны.",
|
| 127 |
+
"network_partition": "Сетевой разрыв или проблемы с связностью между компонентами.",
|
| 128 |
}
|
| 129 |
+
cause = cause_map.get(label, f"Наиболее вероятная категория инцидента: {label}.")
|
| 130 |
checks: List[str] = [
|
| 131 |
+
"Подтвердите временной интервал сбоя в логах и сопоставьте с последними релизами.",
|
| 132 |
+
"Проверьте метрики сервисов/подов (CPU, память, рестарты) вокруг окна инцидента.",
|
| 133 |
+
"Изучите недавние изменения конфигураций и секретов.",
|
| 134 |
]
|
| 135 |
if label == "oom" or "oom" in result.signatures:
|
| 136 |
checks += [
|
| 137 |
+
"Проверьте лимиты/requests памяти контейнера и фактическое потребление.",
|
| 138 |
+
"Если доступны, изучите дампы heap/thread.",
|
| 139 |
+
"Исключите утечки памяти и неограниченные кэши.",
|
| 140 |
+
"Убедитесь, что флаги памяти JVM/рантайма настроены корректно.",
|
| 141 |
]
|
| 142 |
if label in ("timeout",):
|
| 143 |
checks += [
|
| 144 |
+
"Замерьте задержки между сервисом и зависимостями.",
|
| 145 |
+
"Проверьте настройки ретраев/бэк-оффов и circuit breaker.",
|
| 146 |
+
"Поиск потенциально медленных запросов либо перегруженных зависимостей.",
|
| 147 |
]
|
| 148 |
if label in ("auth_failure",):
|
| 149 |
checks += [
|
| 150 |
+
"Проверьте валидность токенов/учётных данных и нужные scope.",
|
| 151 |
+
"Сверьте время между сервисами (clock skew).",
|
| 152 |
+
"Проверьте состояние провайдера аутентификации и его квоты.",
|
| 153 |
]
|
| 154 |
if label in ("db_connection",):
|
| 155 |
checks += [
|
| 156 |
+
"Сопоставьте размер пула БД с текущей нагрузкой.",
|
| 157 |
+
"Проверьте базу на блокировки или медленные запросы.",
|
| 158 |
+
"Убедитесь в корректности host/port/DNS для подключения.",
|
| 159 |
]
|
| 160 |
if label in ("dns_resolution",):
|
| 161 |
checks += [
|
| 162 |
+
"Попробуйте вручную резолвить хост из пода/хоста.",
|
| 163 |
+
"Проверьте здоровье DNS-серверов и свежие изменения записей.",
|
| 164 |
+
"Посмотрите search domains и /etc/resolv.conf внутри контейнера.",
|
| 165 |
]
|
| 166 |
if label in ("tls_handshake",):
|
| 167 |
checks += [
|
| 168 |
+
"Проверьте сертификаты (срок, SAN, цепочка).",
|
| 169 |
+
"Сравните поддерживаемые протоколы/шифры клиента и сервера.",
|
| 170 |
+
"Проверьте настройки ALPN/SNI.",
|
| 171 |
]
|
| 172 |
if label in ("crashloop",):
|
| 173 |
checks += [
|
| 174 |
+
"Проверьте startup/health‑пробы и переопределения команд.",
|
| 175 |
+
"Посмотрите последние логи перед рестартом для поиска первопричины.",
|
| 176 |
+
"Убедитесь, что конфиги/секреты смонтированы и права корректны.",
|
| 177 |
]
|
| 178 |
if retrieved:
|
| 179 |
+
checks.append(f"Изучите ранбук: {retrieved[0]['title']} (сходство {retrieved[0]['score']:.2f}).")
|
| 180 |
# Ensure at least 5 checks
|
| 181 |
while len(checks) < 5:
|
| 182 |
+
checks.append("Добавьте шаг диагностики: снимите дополнительные логи и метрики.")
|
| 183 |
return cause, checks[:10]
|
| 184 |
|
| 185 |
def verify_hypotheses(self, premise: str, hypotheses: List[str]) -> List[Dict]:
|
| 186 |
+
"""
|
| 187 |
+
Прогоняет набор гипотез через NLI, чтобы отметить подтверждение/опровержение.
|
| 188 |
+
"""
|
| 189 |
results = []
|
| 190 |
for hyp in hypotheses:
|
| 191 |
+
raw = self.models.nli({"text": premise, "text_pair": hyp})
|
| 192 |
+
pred = raw[0] if isinstance(raw, list) else raw
|
| 193 |
results.append({"hypothesis": hyp, "label": pred["label"], "score": float(pred["score"])})
|
| 194 |
return results
|
| 195 |
|
|
|
|
| 201 |
use_nli: bool = False,
|
| 202 |
verbosity: int = 1,
|
| 203 |
) -> IncidentResult:
|
| 204 |
+
"""
|
| 205 |
+
Полный пайплайн обработки логов: от предобработки до верификации.
|
| 206 |
+
"""
|
| 207 |
if not raw_text or not raw_text.strip():
|
| 208 |
+
raise ValueError("Поле логов пустое. Пожалуйста, вставьте текст логов или стектрейса.")
|
| 209 |
pre = preprocess_logs(raw_text)
|
| 210 |
cls = self.classify(pre.cleaned_text, source)
|
| 211 |
explanation = self.explain(pre.cleaned_text, verbosity=verbosity)
|
|
|
|
| 213 |
cause, checks = self.generate_cause_and_checks(pre, cls["label"], retrieved)
|
| 214 |
verification = []
|
| 215 |
if use_nli:
|
| 216 |
+
hypotheses = [cause] + [f"Совпадение с ранбуком: {r['title']}" for r in retrieved]
|
| 217 |
verification = self.verify_hypotheses(pre.cleaned_text, hypotheses)
|
| 218 |
return IncidentResult(
|
| 219 |
incident_label=cls["label"],
|
|
|
|
| 229 |
|
| 230 |
|
| 231 |
def serialize_result(result: IncidentResult) -> str:
|
| 232 |
+
"""
|
| 233 |
+
Упаковывает результат в JSON-строку.
|
| 234 |
+
"""
|
| 235 |
return json.dumps(asdict(result), indent=2, ensure_ascii=False)
|
preprocess.py
CHANGED
|
@@ -12,12 +12,18 @@ TIMESTAMP_RE = re.compile(r"\b\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?\b
|
|
| 12 |
|
| 13 |
@dataclass
|
| 14 |
class PreprocessResult:
|
|
|
|
|
|
|
|
|
|
| 15 |
cleaned_text: str
|
| 16 |
signatures: List[str]
|
| 17 |
masked: List[str]
|
| 18 |
|
| 19 |
|
| 20 |
def detect_signatures(text: str) -> List[str]:
|
|
|
|
|
|
|
|
|
|
| 21 |
signatures = []
|
| 22 |
if re.search(r"Traceback|Exception|Error:|Caused by:", text, re.IGNORECASE):
|
| 23 |
signatures.append("stacktrace")
|
|
@@ -35,6 +41,9 @@ def detect_signatures(text: str) -> List[str]:
|
|
| 35 |
|
| 36 |
|
| 37 |
def mask_sensitive(text: str) -> Tuple[str, List[str]]:
|
|
|
|
|
|
|
|
|
|
| 38 |
masked = []
|
| 39 |
|
| 40 |
def _mask(pattern: re.Pattern, placeholder: str, value: str) -> str:
|
|
@@ -51,6 +60,9 @@ def mask_sensitive(text: str) -> Tuple[str, List[str]]:
|
|
| 51 |
|
| 52 |
|
| 53 |
def truncate_logs(text: str, head_lines: int = 120, tail_lines: int = 80, max_lines: int = 400) -> str:
|
|
|
|
|
|
|
|
|
|
| 54 |
lines = text.splitlines()
|
| 55 |
if len(lines) <= max_lines:
|
| 56 |
return text
|
|
@@ -60,6 +72,9 @@ def truncate_logs(text: str, head_lines: int = 120, tail_lines: int = 80, max_li
|
|
| 60 |
|
| 61 |
|
| 62 |
def preprocess_logs(raw_text: str) -> PreprocessResult:
|
|
|
|
|
|
|
|
|
|
| 63 |
normalized = raw_text.strip()
|
| 64 |
truncated = truncate_logs(normalized)
|
| 65 |
masked_text, masked = mask_sensitive(truncated)
|
|
|
|
| 12 |
|
| 13 |
@dataclass
|
| 14 |
class PreprocessResult:
|
| 15 |
+
"""
|
| 16 |
+
Результат предобработки: очищенный текст, сигнатуры и замаскированные значения.
|
| 17 |
+
"""
|
| 18 |
cleaned_text: str
|
| 19 |
signatures: List[str]
|
| 20 |
masked: List[str]
|
| 21 |
|
| 22 |
|
| 23 |
def detect_signatures(text: str) -> List[str]:
|
| 24 |
+
"""
|
| 25 |
+
Ищет в тексте характерные маркеры (стектрейсы, уровни логов и т.д.).
|
| 26 |
+
"""
|
| 27 |
signatures = []
|
| 28 |
if re.search(r"Traceback|Exception|Error:|Caused by:", text, re.IGNORECASE):
|
| 29 |
signatures.append("stacktrace")
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
def mask_sensitive(text: str) -> Tuple[str, List[str]]:
|
| 44 |
+
"""
|
| 45 |
+
Маскирует UUID/IP/почты/пути, возвращая новый текст и список найденных значений.
|
| 46 |
+
"""
|
| 47 |
masked = []
|
| 48 |
|
| 49 |
def _mask(pattern: re.Pattern, placeholder: str, value: str) -> str:
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def truncate_logs(text: str, head_lines: int = 120, tail_lines: int = 80, max_lines: int = 400) -> str:
|
| 63 |
+
"""
|
| 64 |
+
Обрезает длинные логи, сохраняя головы/хвост и вставляя разделитель.
|
| 65 |
+
"""
|
| 66 |
lines = text.splitlines()
|
| 67 |
if len(lines) <= max_lines:
|
| 68 |
return text
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
def preprocess_logs(raw_text: str) -> PreprocessResult:
|
| 75 |
+
"""
|
| 76 |
+
Комплексная подготовка логов к классификации: нормализация, маскировка, сигнатуры.
|
| 77 |
+
"""
|
| 78 |
normalized = raw_text.strip()
|
| 79 |
truncated = truncate_logs(normalized)
|
| 80 |
masked_text, masked = mask_sensitive(truncated)
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
gradio==4.44.
|
| 2 |
transformers==4.38.2
|
| 3 |
torch>=2.2.0,<3.0
|
| 4 |
sentence-transformers==2.5.1
|
|
|
|
| 1 |
+
gradio==4.44.1
|
| 2 |
transformers==4.38.2
|
| 3 |
torch>=2.2.0,<3.0
|
| 4 |
sentence-transformers==2.5.1
|
retrieval.py
CHANGED
|
@@ -9,18 +9,27 @@ from sentence_transformers import CrossEncoder, SentenceTransformer, util
|
|
| 9 |
|
| 10 |
@dataclass
|
| 11 |
class RunbookDoc:
|
|
|
|
|
|
|
|
|
|
| 12 |
path: str
|
| 13 |
title: str
|
| 14 |
content: str
|
| 15 |
|
| 16 |
|
| 17 |
class RunbookRetriever:
|
|
|
|
|
|
|
|
|
|
| 18 |
def __init__(
|
| 19 |
self,
|
| 20 |
kb_dir: str = "kb",
|
| 21 |
embed_model_name: str = "sentence-transformers/all-MiniLM-L6-v2",
|
| 22 |
reranker_name: Optional[str] = "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
| 23 |
):
|
|
|
|
|
|
|
|
|
|
| 24 |
self.kb_dir = kb_dir
|
| 25 |
# Force CPU to avoid CUDA capability mismatches in WSL/GPUs.
|
| 26 |
self.device = torch.device("cpu")
|
|
@@ -42,6 +51,9 @@ class RunbookRetriever:
|
|
| 42 |
self.doc_embeddings = None
|
| 43 |
|
| 44 |
def _load_docs(self) -> List[RunbookDoc]:
|
|
|
|
|
|
|
|
|
|
| 45 |
docs: List[RunbookDoc] = []
|
| 46 |
if not os.path.isdir(self.kb_dir):
|
| 47 |
return docs
|
|
@@ -56,6 +68,9 @@ class RunbookRetriever:
|
|
| 56 |
return docs
|
| 57 |
|
| 58 |
def search(self, query: str, top_k: int = 3):
|
|
|
|
|
|
|
|
|
|
| 59 |
if not self.docs or self.doc_embeddings is None:
|
| 60 |
return []
|
| 61 |
query_emb = self.embed_model.encode(query, convert_to_tensor=True, device=self.device)
|
|
|
|
| 9 |
|
| 10 |
@dataclass
|
| 11 |
class RunbookDoc:
|
| 12 |
+
"""
|
| 13 |
+
Представляет один Markdown-ранбук локальной БЗ.
|
| 14 |
+
"""
|
| 15 |
path: str
|
| 16 |
title: str
|
| 17 |
content: str
|
| 18 |
|
| 19 |
|
| 20 |
class RunbookRetriever:
|
| 21 |
+
"""
|
| 22 |
+
Отвечает за загрузку локальной базы знаний и поиск по ней.
|
| 23 |
+
"""
|
| 24 |
def __init__(
|
| 25 |
self,
|
| 26 |
kb_dir: str = "kb",
|
| 27 |
embed_model_name: str = "sentence-transformers/all-MiniLM-L6-v2",
|
| 28 |
reranker_name: Optional[str] = "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
| 29 |
):
|
| 30 |
+
"""
|
| 31 |
+
Загружает все ранбуки и подготавливает модели (эмбеддер + опциональный reranker).
|
| 32 |
+
"""
|
| 33 |
self.kb_dir = kb_dir
|
| 34 |
# Force CPU to avoid CUDA capability mismatches in WSL/GPUs.
|
| 35 |
self.device = torch.device("cpu")
|
|
|
|
| 51 |
self.doc_embeddings = None
|
| 52 |
|
| 53 |
def _load_docs(self) -> List[RunbookDoc]:
|
| 54 |
+
"""
|
| 55 |
+
Читает Markdown-файлы из kb_dir и превращает их в список RunbookDoc.
|
| 56 |
+
"""
|
| 57 |
docs: List[RunbookDoc] = []
|
| 58 |
if not os.path.isdir(self.kb_dir):
|
| 59 |
return docs
|
|
|
|
| 68 |
return docs
|
| 69 |
|
| 70 |
def search(self, query: str, top_k: int = 3):
|
| 71 |
+
"""
|
| 72 |
+
Находит топ-k релевантных ранбуков по косинусному сходству (и reranker'у, если доступен).
|
| 73 |
+
"""
|
| 74 |
if not self.docs or self.doc_embeddings is None:
|
| 75 |
return []
|
| 76 |
query_emb = self.embed_model.encode(query, convert_to_tensor=True, device=self.device)
|