Первый запуск

This commit is contained in:
KuzarinM
2026-05-02 18:33:38 +03:00
commit cb55eaef01
51 changed files with 2127373 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
from typing import List, Union
from Processor.Models.LogVariable import LogVariable
class LogTemplate:
def __init__(self, uid: int, tokens: List[Union[str, LogVariable]], representative_log: str):
self.uid = uid
self.tokens = tokens
self.representative_log = representative_log
self.embedding = None
self.hits = 1
self.local_var_counter = 1
def get_tokens_as_str_list(self) -> List[str]:
return [str(t) if isinstance(t, LogVariable) else t for t in self.tokens]
def render(self) -> str:
return "".join(str(t) for t in self.tokens)
def get_next_var_id(self) -> int:
vid = self.local_var_counter
self.local_var_counter += 1
return vid

View File

@@ -0,0 +1,12 @@
class LogVariable:
def __init__(self, uid: int, initial_value: str = "", var_type: str = "VAR"):
self.uid = uid
self.initial_value = initial_value
self.var_type = var_type
def __str__(self):
return f"<{self.var_type}_{self.uid}>"
def __repr__(self):
return str(self)

View File

@@ -0,0 +1,417 @@
import difflib
import os
import re
import time
from typing import List, Dict, Any, Union, Optional
import numpy as np
from sentence_transformers import SentenceTransformer, util
from Processor.Models.LogTemplate import LogTemplate
from Processor.Models.LogVariable import LogVariable
from Processor.TemplateDatabase import TemplateDatabase
class StreamingLogCluster:
# --- Константы класса для удобства настройки ---
THRESHOLD_CREATE_NEW = 0.7 #0.70
SCORE_EXACT_MATCH = 0.85
SCORE_PARTIAL_MATCH = 0.6
MAX_VAR_LEN = 32
HARD_DELIMITERS = {'|', ';', ','}
SOFT_DELIMITERS = {'=', ':', '-', '>', '<', '[', ']', '(', ')', '{', '}', '"', "'"}
def __init__(self, model_path: str, db_path: str = "logs_knowledge.db"):
self.model = SentenceTransformer(model_path)
self.db = TemplateDatabase(db_path)
# Компилируем регулярные выражения один раз
self.mask_regex = {
'guid': re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...'),
'ip': re.compile(r'\d{1,3}(?:\.\d{1,3}){3}'),
'ver': re.compile(r'\d{1,3}(?:\.\d{1,3}){2}'),
'num': re.compile(r'-?\d+(\.\d+)?'),
'base64': re.compile(r'(?<![A-Za-z0-9+/])(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?(?![A-Za-z0-9+/])')
}
token_patterns = [
r'(?P<DATE>\d{4}-\d{2}-\d{2}|\d{2}\.\d{2}\.\d{4}|\d{2}/\d{2}/\d{4})',
r'(?P<TIME>\d{2}:\d{2}:\d{2}(?:\.\d+)?)',
r'(?P<EMAIL>[\w\.-]+@[\w\.-]+\.\w+)',
r'(?P<IP>\d{1,3}(?:\.\d{1,3}){3})',
r'(?P<VER>\d{1,3}(?:\.\d{1,3}){2})',
r'(?P<GUID>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...)',
r'(?P<WORD>[a-zA-Z0-9_]+)',
r'(?P<SYMBOL>[^\w\s])',
r'(?P<SPACE>\s+)'
]
self.master_regex = re.compile('|'.join(token_patterns))
self.var_type_names = {'DATE', 'TIME', 'EMAIL', 'IP', 'GUID', "VER"}
# --- Легковесный индекс в ОЗУ ---
self.template_ids: List[int] = []
self.embeddings: Optional[np.ndarray] = None
self.template_id_counter = self.db.get_max_id() + 1
self._load_index()
def _load_index(self):
"""Загружает ТОЛЬКО векторы и ID из БД, экономя оперативную память."""
print("📥 Загрузка векторного индекса из БД...")
# Принимаем в одну переменную (это просто список)
index_data = self.db.load_index_data()
# Если список пуст (БД пустая), безопасно выходим
if not index_data:
print("✅ База пуста.")
self.template_ids = []
self.embeddings = None
return
raw_templates, _ = index_data
ids = []
vecs = []
for row in raw_templates:
uid, _, emb_blob, _, _ = row
ids.append(uid)
vecs.append(np.frombuffer(emb_blob, dtype=np.float32))
self.template_ids = ids
self.embeddings = np.array(vecs)
print(f"✅ Готово. В индексе шаблонов: {len(self.template_ids)}")
def close(self):
self.db.close()
# --- Утилиты ---
def _tokenize(self, text: str) -> List[str]:
return [m.group() for m in self.master_regex.finditer(text)]
def _mask_for_search(self, text: str) -> str:
text = self.mask_regex['guid'].sub('<GUID>', text)
text = self.mask_regex['ip'].sub('<IP>', text)
text = self.mask_regex['num'].sub('<NUM>', text)
return text
def _detect_var_type(self, value: str) -> str:
match = self.master_regex.fullmatch(value)
return match.lastgroup if match and match.lastgroup in self.var_type_names else "VAR"
# --- Логика Кластеризации ---
def _find_best_match(self, input_vec: np.ndarray, log_text: str) -> Optional[int]:
"""Ищет лучший шаблон по косинусному сходству, используя только RAM-индекс."""
if self.embeddings is None or len(self.template_ids) == 0:
return None
scores = util.cos_sim(input_vec, self.embeddings)[0]
best_idx = scores.argmax().item()
best_score = scores[best_idx].item()
best_id = self.template_ids[best_idx]
if best_score > self.SCORE_EXACT_MATCH:
return best_id
if best_score > self.SCORE_PARTIAL_MATCH:
# Для проверки токенов придется подгрузить кандидата из БД
cand = self. _load_template_from_db(best_id)
cand_tokens = cand.get_tokens_as_str_list()
new_tokens = self._tokenize(log_text)
ratio = difflib.SequenceMatcher(None, cand_tokens, new_tokens).ratio()
if ratio > self.THRESHOLD_CREATE_NEW:
return best_id
return None
def process(self, log_text: str) -> Dict[str, Any]:
"""Основной пайплайн обработки лога."""
masked_input = self._mask_for_search(log_text)
input_vec = self.model.encode(masked_input)
best_id = self._find_best_match(input_vec, log_text)
if best_id is not None:
# Шаблон найден -> Грузим его из БД (ленивая загрузка)
template = self._load_template_from_db(best_id)
# Обновляем вектор скользящим средним
n = template.hits
updated_vec = (template.embedding * n + input_vec) / (n + 1)
template.embedding = updated_vec
# Обновляем вектор в RAM
idx = self.template_ids.index(best_id)
self.embeddings[idx] = updated_vec
return self._update_and_extract(template, log_text)
else:
# Шаблон не найден -> Создаем новый
return self._create_new_template(log_text, input_vec)
def process_time_measure(self,log_text: str) -> (float, float, float):
"""Основной пайплайн обработки лога."""
t1 = time.time()
masked_input = self._mask_for_search(log_text)
input_vec = self.model.encode(masked_input)
t2 = time.time()
best_id = self._find_best_match(input_vec, log_text)
if best_id is not None:
# Шаблон найден -> Грузим его из БД (ленивая загрузка)
template = self._load_template_from_db(best_id)
# Обновляем вектор скользящим средним
n = template.hits
updated_vec = (template.embedding * n + input_vec) / (n + 1)
template.embedding = updated_vec
# Обновляем вектор в RAM
idx = self.template_ids.index(best_id)
self.embeddings[idx] = updated_vec
t3 = time.time()
self._update_and_extract(template, log_text)
else:
t3 = time.time()
# Шаблон не найден -> Создаем новый
self._create_new_template(log_text, input_vec)
t4 = time.time()
return t2-t1, t3-t2, t4-t3
# --- Создание и обновление шаблонов ---
def _create_new_template(self, log_text: str, vector: np.ndarray) -> Dict[str, Any]:
tokens = self._tokenize(log_text)
new_tpl = LogTemplate(self.template_id_counter, tokens, log_text)
new_tpl.embedding = vector
# Добавляем в RAM индекс
self.template_ids.append(new_tpl.uid)
if self.embeddings is None:
self.embeddings = np.array([vector])
else:
self.embeddings = np.vstack([self.embeddings, vector])
self.template_id_counter += 1
self.db.save_template(new_tpl)
return {
'template_id': new_tpl.uid,
'template_view': new_tpl.render(),
'variables': [],
'status': 'created'
}
def _update_and_extract(self, template: LogTemplate, log_text: str) -> Dict[str, Any]:
new_tokens = self._tokenize(log_text)
old_tokens_str = template.get_tokens_as_str_list()
matcher = difflib.SequenceMatcher(None, old_tokens_str, new_tokens)
updated_template_tokens = []
extracted_variables = []
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
updated_template_tokens.extend(template.tokens[i1:i2])
elif tag == 'replace':
log_vals = new_tokens[j1:j2]
tpl_seg = template.tokens[i1:i2]
# Если заменяем существующую переменную
if len(tpl_seg) == 1 and isinstance(tpl_seg[0], LogVariable):
var = tpl_seg[0]
full_text = "".join(log_vals)
is_bloated = len(full_text) > self.MAX_VAR_LEN
has_hard = any(t.strip() in self.HARD_DELIMITERS for t in log_vals)
has_space = any(t.isspace() for t in log_vals)
has_soft = any(t.strip() in self.SOFT_DELIMITERS for t in log_vals)
if has_hard or has_space or (is_bloated and has_soft):
decomposed, new_vars = self._decompose_segment(log_vals, template, var.initial_value)
updated_template_tokens.extend(decomposed)
extracted_variables.extend(new_vars)
else:
updated_template_tokens.append(var)
if full_text != var.initial_value:
extracted_variables.append(self._make_delta(var, full_text))
else:
# Заменяем текст -> формируем новые переменные
init_hint = "".join(t.initial_value if isinstance(t, LogVariable) else str(t) for t in tpl_seg)
decomposed, new_vars = self._decompose_segment(log_vals, template, init_hint)
updated_template_tokens.extend(decomposed)
extracted_variables.extend(new_vars)
elif tag == 'delete':
tpl_seg = template.tokens[i1:i2]
if len(tpl_seg) == 1 and isinstance(tpl_seg[0], LogVariable):
var = tpl_seg[0]
updated_template_tokens.append(var)
if var.initial_value != "":
extracted_variables.append(self._make_delta(var, ""))
else:
new_var = LogVariable(template.get_next_var_id(), initial_value="".join(str(t) for t in tpl_seg))
updated_template_tokens.append(new_var)
if new_var.initial_value != "":
extracted_variables.append(self._make_delta(new_var, ""))
elif tag == 'insert':
decomposed, new_vars = self._decompose_segment(new_tokens[j1:j2], template, "")
updated_template_tokens.extend(decomposed)
extracted_variables.extend(new_vars)
template.tokens = updated_template_tokens
template.hits += 1
self.db.save_template(template)
return {
'template_id': template.uid,
'template_view': template.render(),
'variables': extracted_variables,
'status': 'updated'
}
# --- Вспомогательные методы для логики извлечения ---
def _decompose_segment(self, tokens_list: List[str], template: LogTemplate, initial_hint: str):
"""Разбивает сегмент на переменные и статические токены."""
full_text = "".join(tokens_list)
is_bloated = len(full_text) > self.MAX_VAR_LEN
result_structure = []
extracted_vars = []
current_var_tokens = []
def flush_var():
if not current_var_tokens:
return
val = "".join(current_var_tokens)
v_type = self._detect_var_type(val)
init = initial_hint if len(result_structure) == 0 else ""
new_v = LogVariable(template.get_next_var_id(), initial_value=init, var_type=v_type)
result_structure.append(new_v)
if val != new_v.initial_value:
extracted_vars.append(self._make_delta(new_v, val))
current_var_tokens.clear()
for token in tokens_list:
t_strip = token.strip()
should_split = (t_strip in self.HARD_DELIMITERS) or token.isspace() or (
is_bloated and t_strip in self.SOFT_DELIMITERS)
if should_split:
flush_var()
result_structure.append(token)
else:
current_var_tokens.append(token)
flush_var()
return result_structure, extracted_vars
def _make_delta(self, var: LogVariable, actual_value: str) -> Dict[str, Any]:
"""Формирует словарь дельты (изменения) для переменной."""
return {
'uid': var.uid,
'name': str(var),
'value': actual_value,
'initial': var.initial_value
}
# --- Интеграция с БД (Ленивая загрузка) ---
def _load_template_from_db(self, uid: int) -> LogTemplate:
"""Восстанавливает конкретный шаблон из БД."""
row, vars_map = self.db.get_template_data_by_id(uid)
if not row:
raise ValueError(f"Шаблон с ID {uid} не найден в БД!")
template_id, pattern, emb_blob, hits, local_cnt = row
# Передаем vars_map напрямую, так как там уже лежат переменные только этого шаблона
tokens = self._hydrate_pattern(pattern, vars_map)
tpl = LogTemplate(template_id, tokens, pattern)
tpl.embedding = np.frombuffer(emb_blob, dtype=np.float32)
tpl.hits = hits
tpl.local_var_counter = local_cnt
return tpl
def _hydrate_pattern(self, pattern: str, tpl_vars: Dict[int, LogVariable]) -> List:
parts = re.split(r'(<[A-Z]+_\d+>)', pattern)
tokens = []
for part in parts:
if not part: continue
if part.startswith('<') and part.endswith('>'):
match = re.match(r'<([A-Z]+)_(\d+)>', part)
if match:
v_type, v_id_str = match.groups()
v_id = int(v_id_str)
if v_id in tpl_vars:
tokens.append(tpl_vars[v_id])
else:
tokens.append(LogVariable(v_id, var_type=v_type))
continue
tokens.extend(self._tokenize(part))
return tokens
if __name__ == '__main__':
MODEL_PATH = '../Resources/model'
DB_FILE = "logs.db"
if os.path.exists(DB_FILE):
os.remove(DB_FILE)
print("--- ЗАПУСК: Delta Mode ---")
clusterer = StreamingLogCluster(MODEL_PATH, db_path=DB_FILE)
# 1. Создаем шаблон.
# Переменных нет, так как все значения становятся "дефолтными" (initial).
log1 = "2025-01-01 User admin login"
res1 = clusterer.process(log1)
print(f"Log 1: {log1} -> ID: {res1['template_id']}")
print(f" VARS (Delta): {res1['variables']}")
# Ожидание: [], так как при создании шаблона текущие значения становятся Initial.
# 2. Меняем admin -> guest.
# Должна вернуться ТОЛЬКО переменная гостя. Дата та же - она не вернется!
log2 = "2025-01-01 User guest login"
res2 = clusterer.process(log2)
print(f"\nLog 2: {log2} -> ID: {res2['template_id']}")
# Красивый вывод дельты
if res2['variables']:
print(" CHANGES DETECTED:")
for v in res2['variables']:
print(f" * {v['name']} changed from '{v['initial']}' to '{v['value']}'")
else:
print(" NO CHANGES (Full match with template defaults)")
# 3. Меняем всё (Дата + Юзер)
log3 = "2025-02-02 User root login"
res3 = clusterer.process(log3)
print(f"\nLog 3: {log3} -> ID: {res3['template_id']}")
if res3['variables']:
print(" CHANGES DETECTED:")
for v in res3['variables']:
print(f" * {v['name']} ('{v['initial']}') to '{v['value']}'")
# 4. Возвращаемся к оригиналу (admin + старая дата)
# Должен вернуться пустой список, так как это идеальное совпадение с Initials
log4 = "2025-01-01 User admin login"
res4 = clusterer.process(log4)
print(f"\nLog 4 (Revert): {log4} -> ID: {res4['template_id']}")
print(f" VARS (Delta): {res4['variables']}")

View File

@@ -0,0 +1,122 @@
import re
import sqlite3
import numpy as np
from typing import List, Dict, Tuple, Optional
from Processor.Models.LogTemplate import LogTemplate
from Processor.Models.LogVariable import LogVariable
class TemplateDatabase:
def __init__(self, db_path: str = "logs_knowledge.db"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.create_tables()
def create_tables(self):
with self.conn:
self.conn.execute("""
CREATE TABLE IF NOT EXISTS templates (
id INTEGER PRIMARY KEY,
pattern TEXT NOT NULL,
embedding BLOB NOT NULL,
hits INTEGER DEFAULT 1,
local_counter INTEGER DEFAULT 1
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS variables (
template_id INTEGER,
local_id INTEGER,
var_type TEXT,
initial_value TEXT,
PRIMARY KEY (template_id, local_id),
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE
)
""")
def save_template(self, tpl: LogTemplate):
emb_bytes = tpl.embedding.astype(np.float32).tobytes()
pattern_str = tpl.render()
with self.conn:
self.conn.execute("""
INSERT INTO templates (id, pattern, embedding, hits, local_counter)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
pattern = excluded.pattern,
embedding = excluded.embedding,
hits = excluded.hits,
local_counter = excluded.local_counter
""", (tpl.uid, pattern_str, emb_bytes, tpl.hits, tpl.local_var_counter))
self.conn.execute("DELETE FROM variables WHERE template_id = ?", (tpl.uid,))
vars_data = []
for token in tpl.tokens:
if isinstance(token, LogVariable):
vars_data.append((tpl.uid, token.uid, token.var_type, token.initial_value))
if vars_data:
self.conn.executemany("INSERT INTO variables VALUES (?, ?, ?, ?)", vars_data)
# --- НОВЫЕ МЕТОДЫ ДЛЯ ОПТИМИЗАЦИИ ОЗУ ---
def load_index_data(self) -> List[Tuple[int, bytes]]:
"""
Загружает ТОЛЬКО идентификаторы и эмбеддинги.
Используется при старте приложения для построения RAM-индекса.
"""
cursor = self.conn.execute("SELECT id, embedding FROM templates")
return cursor.fetchall()
def get_template_data_by_id(self, template_id: int) -> Tuple[Optional[Tuple], Dict[int, LogVariable]]:
"""
Точечно загружает сырые данные ОДНОГО шаблона по его ID.
Возвращает: (row_шаблона, словарь_переменных)
"""
# 1. Загружаем сам шаблон
cursor = self.conn.execute(
"SELECT id, pattern, embedding, hits, local_counter FROM templates WHERE id = ?",
(template_id,)
)
row = cursor.fetchone()
if not row:
return None, {}
# 2. Загружаем его переменные
vars_cursor = self.conn.execute(
"SELECT local_id, var_type, initial_value FROM variables WHERE template_id = ?",
(template_id,)
)
vars_map = {}
for v_row in vars_cursor:
l_id, v_type, init_val = v_row
vars_map[l_id] = LogVariable(l_id, initial_value=init_val, var_type=v_type)
return row, vars_map
def load_raw_data(self):
"""Возвращает все данные целиком. (Осторожно: может забить ОЗУ при большом объеме БД)"""
cursor = self.conn.execute("SELECT template_id, local_id, var_type, initial_value FROM variables")
vars_map = {}
for row in cursor:
t_id, l_id, v_type, init_val = row
if t_id not in vars_map: vars_map[t_id] = {}
vars_map[t_id][l_id] = LogVariable(l_id, initial_value=init_val, var_type=v_type)
templates_data = []
cursor = self.conn.execute("SELECT id, pattern, embedding, hits, local_counter FROM templates")
for row in cursor:
templates_data.append(row)
return templates_data, vars_map
def get_max_id(self) -> int:
res = self.conn.execute("SELECT MAX(id) FROM templates").fetchone()[0]
return res if res else 0
def close(self):
self.conn.close()

BIN
Processor/logs.db Normal file

Binary file not shown.