updated view and database
This commit is contained in:
@@ -27,7 +27,7 @@ class AnsibleService:
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.db = get_db()
|
||||
self._db = None
|
||||
self.ansible_dir = Path("ansible")
|
||||
self.inventory_file = self.ansible_dir / "inventory" / "dynamic_inventory.yaml"
|
||||
self.playbook_dir = self.ansible_dir / "playbooks"
|
||||
@@ -42,6 +42,13 @@ class AnsibleService:
|
||||
(self.ansible_dir / "roles").mkdir(exist_ok=True)
|
||||
self.ssh_keys_dir.mkdir(mode=0o700, exist_ok=True)
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
"""Lazily resolve the database handle (see LogCompressionService.db)."""
|
||||
if self._db is None:
|
||||
self._db = get_db()
|
||||
return self._db
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Settings helpers #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -13,8 +13,15 @@ class DeviceService:
|
||||
"""Service for managing devices and device-related operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = get_db()
|
||||
self._db = None
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
"""Lazily resolve the database handle (see LogCompressionService.db)."""
|
||||
if self._db is None:
|
||||
self._db = get_db()
|
||||
return self._db
|
||||
|
||||
# Basic CRUD Operations
|
||||
|
||||
@@ -213,7 +220,10 @@ class DeviceService:
|
||||
'recent_logs_24h': recent_logs,
|
||||
'total_files': total_files,
|
||||
'last_log': last_log,
|
||||
'uptime_days': (datetime.utcnow() - device.last_seen).days if device.last_seen else 0
|
||||
# Days since the device was last seen (0 = seen today / currently active).
|
||||
# Previously mislabeled as "uptime_days" which implied the opposite.
|
||||
'days_since_last_seen': (datetime.utcnow() - device.last_seen).days if device.last_seen else None,
|
||||
'is_online': device.is_online,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -9,13 +9,14 @@ from pathlib import Path
|
||||
from werkzeug.utils import secure_filename
|
||||
from app.models import Device, FileUpload
|
||||
from config.database_config import get_db
|
||||
from sqlalchemy import func
|
||||
import logging
|
||||
|
||||
class FileUploadService:
|
||||
"""Service for handling file uploads and processing"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = get_db()
|
||||
self._db = None
|
||||
self.upload_folder = Path("data/uploads")
|
||||
self.upload_folder.mkdir(exist_ok=True)
|
||||
|
||||
@@ -27,7 +28,14 @@ class FileUploadService:
|
||||
|
||||
# Max file size (50MB)
|
||||
self.max_file_size = 50 * 1024 * 1024
|
||||
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
"""Lazily resolve the database handle (see LogCompressionService.db)."""
|
||||
if self._db is None:
|
||||
self._db = get_db()
|
||||
return self._db
|
||||
|
||||
def process_uploaded_file(self, file, device_info):
|
||||
"""Process uploaded file from device"""
|
||||
try:
|
||||
@@ -105,7 +113,7 @@ class FileUploadService:
|
||||
|
||||
# Process file content if it's a log file
|
||||
if self._is_log_file(filename, mime_type):
|
||||
self._process_log_file(file_upload, file_content)
|
||||
self._process_log_file(session, file_upload, file_content)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@@ -176,51 +184,50 @@ class FileUploadService:
|
||||
|
||||
return False
|
||||
|
||||
def _process_log_file(self, file_upload, content):
|
||||
"""Process log file content to extract log entries"""
|
||||
def _process_log_file(self, session, file_upload, content):
|
||||
"""Process log file content to extract log entries.
|
||||
|
||||
Reuses the caller's database session (no nested per-line sessions) by
|
||||
calling LogCompressionService.store_log_in_session for every line.
|
||||
"""
|
||||
try:
|
||||
# Mark as log file
|
||||
file_upload.is_log_file = True
|
||||
|
||||
|
||||
# Simple log processing - split by lines
|
||||
lines = content.decode('utf-8', errors='ignore').split('\n')
|
||||
entries_extracted = 0
|
||||
|
||||
|
||||
from app.services.log_service import LogCompressionService
|
||||
log_service = LogCompressionService()
|
||||
|
||||
device_info = {
|
||||
'hostname': file_upload.device.hostname,
|
||||
'device_ip': file_upload.device.device_ip,
|
||||
'nume_masa': file_upload.device.nume_masa
|
||||
}
|
||||
|
||||
|
||||
device = file_upload.device
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
|
||||
# Try to extract timestamp and message
|
||||
# This is a simple implementation - enhance as needed
|
||||
message = f"[File: {file_upload.original_filename}:{line_num}] {line}"
|
||||
|
||||
# Process through log compression service
|
||||
result = log_service.process_log_message(
|
||||
device_info=device_info,
|
||||
|
||||
# Store within the SAME session – avoids opening a session per line
|
||||
log_service.store_log_in_session(
|
||||
session=session,
|
||||
device=device,
|
||||
message=message,
|
||||
severity='info'
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
entries_extracted += 1
|
||||
|
||||
entries_extracted += 1
|
||||
|
||||
# Update file record
|
||||
file_upload.log_entries_extracted = entries_extracted
|
||||
file_upload.processed = True
|
||||
file_upload.processing_status = 'completed'
|
||||
|
||||
|
||||
logging.info(f"Processed log file {file_upload.filename}: {entries_extracted} entries extracted")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing log file content: {e}")
|
||||
file_upload.processing_status = 'error'
|
||||
@@ -235,7 +242,7 @@ class FileUploadService:
|
||||
|
||||
# Calculate total size
|
||||
total_size = session.query(
|
||||
session.func.sum(FileUpload.file_size)
|
||||
func.sum(FileUpload.file_size)
|
||||
).scalar() or 0
|
||||
|
||||
# Count by processing status
|
||||
|
||||
+134
-112
@@ -15,47 +15,66 @@ class LogCompressionService:
|
||||
"""Service for compressing log messages using templates and aliases"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = get_db()
|
||||
self._db = None
|
||||
self.template_patterns = self._load_common_patterns()
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
"""Lazily resolve the database handle so creating this service at import
|
||||
time does not initialize the DB before the Flask app context exists."""
|
||||
if self._db is None:
|
||||
self._db = get_db()
|
||||
return self._db
|
||||
|
||||
def _load_common_patterns(self) -> List[Dict]:
|
||||
"""Load common log message patterns for template matching"""
|
||||
"""Load common log message patterns for template matching.
|
||||
|
||||
Each entry's 'variables' list maps regex capture groups (in order) to
|
||||
template variable names, so matching/extraction is fully data-driven and
|
||||
no longer relies on a hardcoded if/elif chain.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'pattern': r'Card detected: ([A-F0-9]+)',
|
||||
'template': 'Card detected: {card_id}',
|
||||
'category': 'card_detection',
|
||||
'alias_prefix': 'CD'
|
||||
'alias_prefix': 'CD',
|
||||
'variables': ['card_id'],
|
||||
},
|
||||
{
|
||||
'pattern': r'Connection failed: (.+)',
|
||||
'template': 'Connection failed: {error}',
|
||||
'category': 'connection_error',
|
||||
'alias_prefix': 'CE'
|
||||
'alias_prefix': 'CE',
|
||||
'variables': ['error'],
|
||||
},
|
||||
{
|
||||
'pattern': r'System startup completed in ([0-9.]+)s',
|
||||
'template': 'System startup completed in {time}s',
|
||||
'category': 'system_startup',
|
||||
'alias_prefix': 'SS'
|
||||
'alias_prefix': 'SS',
|
||||
'variables': ['time'],
|
||||
},
|
||||
{
|
||||
'pattern': r'Auto-update: (.+)',
|
||||
'template': 'Auto-update: {message}',
|
||||
'category': 'auto_update',
|
||||
'alias_prefix': 'AU'
|
||||
'alias_prefix': 'AU',
|
||||
'variables': ['message'],
|
||||
},
|
||||
{
|
||||
'pattern': r'Command \'([^\']+)\' (SUCCESS|FAILED)',
|
||||
'template': 'Command \'{command}\' {status}',
|
||||
'category': 'command_execution',
|
||||
'alias_prefix': 'EX'
|
||||
'alias_prefix': 'EX',
|
||||
'variables': ['command', 'status'],
|
||||
},
|
||||
{
|
||||
'pattern': r'Temperature: ([0-9.]+)°C',
|
||||
'template': 'Temperature: {temp}°C',
|
||||
'category': 'temperature',
|
||||
'alias_prefix': 'TM'
|
||||
'alias_prefix': 'TM',
|
||||
'variables': ['temp'],
|
||||
}
|
||||
]
|
||||
|
||||
@@ -75,75 +94,12 @@ class LogCompressionService:
|
||||
with self.db.get_session() as session:
|
||||
# Get or create device
|
||||
device = self._get_or_create_device(session, device_info)
|
||||
|
||||
# Try to match message to existing template
|
||||
template, variables = self._match_message_template(session, message)
|
||||
|
||||
if template:
|
||||
# Use existing template
|
||||
log_entry = LogEntry(
|
||||
device_id=device.id,
|
||||
template_id=template.id,
|
||||
template_variables=json.dumps(variables) if variables else None,
|
||||
severity=severity,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
# Update template usage count
|
||||
template.usage_count += 1
|
||||
|
||||
# Calculate size savings
|
||||
original_size = len(message.encode('utf-8'))
|
||||
compressed_size = len(template.alias.encode('utf-8')) + \
|
||||
len(json.dumps(variables or {}).encode('utf-8'))
|
||||
|
||||
compression_info = {
|
||||
'used_template': True,
|
||||
'template_alias': template.alias,
|
||||
'original_size': original_size,
|
||||
'compressed_size': compressed_size,
|
||||
'savings_percent': ((original_size - compressed_size) / original_size) * 100
|
||||
}
|
||||
else:
|
||||
# Create new template if message matches a pattern
|
||||
template = self._create_new_template(session, message)
|
||||
|
||||
if template:
|
||||
# New template created
|
||||
variables = self._extract_variables(message, template.template_text)
|
||||
log_entry = LogEntry(
|
||||
device_id=device.id,
|
||||
template_id=template.id,
|
||||
template_variables=json.dumps(variables) if variables else None,
|
||||
severity=severity,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
template.usage_count = 1
|
||||
|
||||
compression_info = {
|
||||
'used_template': True,
|
||||
'template_alias': template.alias,
|
||||
'new_template': True,
|
||||
'original_size': len(message.encode('utf-8')),
|
||||
'compressed_size': len(template.alias.encode('utf-8'))
|
||||
}
|
||||
else:
|
||||
# Store as full message
|
||||
log_entry = LogEntry(
|
||||
device_id=device.id,
|
||||
full_message=message,
|
||||
severity=severity,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
compression_info = {
|
||||
'used_template': False,
|
||||
'stored_full': True,
|
||||
'original_size': len(message.encode('utf-8'))
|
||||
}
|
||||
|
||||
session.add(log_entry)
|
||||
|
||||
log_entry, compression_info = self.store_log_in_session(
|
||||
session, device, message, severity
|
||||
)
|
||||
session.flush() # Get the log entry ID
|
||||
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'log_id': log_entry.id,
|
||||
@@ -159,7 +115,73 @@ class LogCompressionService:
|
||||
'error': str(e),
|
||||
'message': 'Log processing failed'
|
||||
}
|
||||
|
||||
|
||||
def store_log_in_session(self, session: Session, device: Device,
|
||||
message: str, severity: str = 'info') -> Tuple[LogEntry, Dict]:
|
||||
"""Compress and store a single log message using an existing session/device.
|
||||
|
||||
Shared by process_log_message() and bulk file ingestion so callers don't
|
||||
open a new database session per log line. The caller is responsible for
|
||||
committing (the session context manager does this).
|
||||
Returns (log_entry, compression_info).
|
||||
"""
|
||||
# Try to match message to existing template
|
||||
template, variables = self._match_message_template(session, message)
|
||||
|
||||
if template:
|
||||
log_entry = LogEntry(
|
||||
device_id=device.id,
|
||||
template_id=template.id,
|
||||
template_variables=json.dumps(variables) if variables else None,
|
||||
severity=severity,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
template.usage_count += 1
|
||||
|
||||
original_size = len(message.encode('utf-8'))
|
||||
compressed_size = len(template.alias.encode('utf-8')) + \
|
||||
len(json.dumps(variables or {}).encode('utf-8'))
|
||||
compression_info = {
|
||||
'used_template': True,
|
||||
'template_alias': template.alias,
|
||||
'original_size': original_size,
|
||||
'compressed_size': compressed_size,
|
||||
'savings_percent': ((original_size - compressed_size) / original_size) * 100
|
||||
}
|
||||
else:
|
||||
template, variables = self._create_new_template(session, message)
|
||||
if template:
|
||||
log_entry = LogEntry(
|
||||
device_id=device.id,
|
||||
template_id=template.id,
|
||||
template_variables=json.dumps(variables) if variables else None,
|
||||
severity=severity,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
template.usage_count = 1
|
||||
compression_info = {
|
||||
'used_template': True,
|
||||
'template_alias': template.alias,
|
||||
'new_template': True,
|
||||
'original_size': len(message.encode('utf-8')),
|
||||
'compressed_size': len(template.alias.encode('utf-8'))
|
||||
}
|
||||
else:
|
||||
log_entry = LogEntry(
|
||||
device_id=device.id,
|
||||
full_message=message,
|
||||
severity=severity,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
compression_info = {
|
||||
'used_template': False,
|
||||
'stored_full': True,
|
||||
'original_size': len(message.encode('utf-8'))
|
||||
}
|
||||
|
||||
session.add(log_entry)
|
||||
return log_entry, compression_info
|
||||
|
||||
@staticmethod
|
||||
def _infer_device_type(hostname: str) -> str:
|
||||
"""Guess device type from hostname pattern."""
|
||||
@@ -250,49 +272,37 @@ class LogCompressionService:
|
||||
template_text=template_text,
|
||||
category=pattern_info['category']
|
||||
).first()
|
||||
|
||||
|
||||
if template:
|
||||
# Extract variables
|
||||
variables = {}
|
||||
for i, group in enumerate(match.groups(), 1):
|
||||
# Map to variable names based on template
|
||||
if '{card_id}' in template_text and pattern_info['category'] == 'card_detection':
|
||||
variables['card_id'] = group
|
||||
elif '{error}' in template_text and pattern_info['category'] == 'connection_error':
|
||||
variables['error'] = group
|
||||
elif '{time}' in template_text and pattern_info['category'] == 'system_startup':
|
||||
variables['time'] = group
|
||||
elif '{message}' in template_text:
|
||||
variables['message'] = group
|
||||
elif '{command}' in template_text and i == 1:
|
||||
variables['command'] = group
|
||||
elif '{status}' in template_text and i == 2:
|
||||
variables['status'] = group
|
||||
elif '{temp}' in template_text:
|
||||
variables['temp'] = group
|
||||
|
||||
variables = self._variables_from_match(pattern_info, match)
|
||||
return template, variables
|
||||
|
||||
|
||||
return None, None
|
||||
|
||||
def _create_new_template(self, session: Session, message: str) -> Optional[MessageTemplate]:
|
||||
"""Create new template if message matches a known pattern"""
|
||||
def _create_new_template(self, session: Session, message: str) -> Tuple[Optional[MessageTemplate], Optional[Dict]]:
|
||||
"""Create new template if message matches a known pattern.
|
||||
|
||||
Returns (template, variables) so the caller can persist the extracted
|
||||
variables on the first log entry that creates the template.
|
||||
"""
|
||||
for pattern_info in self.template_patterns:
|
||||
match = re.match(pattern_info['pattern'], message)
|
||||
if match:
|
||||
variables = self._variables_from_match(pattern_info, match)
|
||||
|
||||
# Check if template already exists
|
||||
existing = session.query(MessageTemplate).filter_by(
|
||||
template_text=pattern_info['template'],
|
||||
category=pattern_info['category']
|
||||
).first()
|
||||
|
||||
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
return existing, variables
|
||||
|
||||
# Create new template
|
||||
alias = self._generate_alias(session, pattern_info['alias_prefix'])
|
||||
template_hash = MessageTemplate.create_hash(pattern_info['template'])
|
||||
|
||||
|
||||
template = MessageTemplate(
|
||||
template_hash=template_hash,
|
||||
template_text=pattern_info['template'],
|
||||
@@ -303,9 +313,9 @@ class LogCompressionService:
|
||||
|
||||
session.add(template)
|
||||
session.flush()
|
||||
return template
|
||||
return template, variables
|
||||
|
||||
return None
|
||||
return None, None
|
||||
|
||||
def _generate_alias(self, session: Session, prefix: str) -> str:
|
||||
"""Generate unique alias for template"""
|
||||
@@ -325,12 +335,24 @@ class LogCompressionService:
|
||||
return f"{prefix}{max_num + 1:03d}"
|
||||
|
||||
def _extract_variables(self, message: str, template: str) -> Dict:
|
||||
"""Extract variables from message using template"""
|
||||
# Simple variable extraction - could be enhanced
|
||||
variables = {}
|
||||
# This is a simplified implementation
|
||||
# In production, you'd want more sophisticated template matching
|
||||
return variables
|
||||
"""Extract template variables from a raw message.
|
||||
|
||||
Finds the matching pattern for the message and maps its capture groups
|
||||
to the configured variable names. Returns {} when no pattern matches.
|
||||
"""
|
||||
for pattern_info in self.template_patterns:
|
||||
if pattern_info['template'] != template:
|
||||
continue
|
||||
match = re.match(pattern_info['pattern'], message)
|
||||
if match:
|
||||
return self._variables_from_match(pattern_info, match)
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _variables_from_match(pattern_info: Dict, match) -> Dict:
|
||||
"""Map a regex match's capture groups to named template variables."""
|
||||
names = pattern_info.get('variables') or []
|
||||
return {name: value for name, value in zip(names, match.groups())}
|
||||
|
||||
def get_compression_stats(self) -> Dict:
|
||||
"""Get compression statistics"""
|
||||
|
||||
Reference in New Issue
Block a user