373 lines
13 KiB
Python
373 lines
13 KiB
Python
"""
|
||
Enhanced API endpoints for logs with compression and file support
|
||
"""
|
||
from flask import Blueprint, request, jsonify
|
||
from werkzeug.utils import secure_filename
|
||
import os
|
||
import hashlib
|
||
from datetime import datetime
|
||
from app.services.log_service import LogCompressionService
|
||
from app.services.file_service import FileUploadService
|
||
from app.models import Device, LogEntry, FileUpload
|
||
from config.database_config import get_db
|
||
import logging
|
||
|
||
# Create blueprint
|
||
logs_bp = Blueprint('logs', __name__, url_prefix='/api/logs')
|
||
|
||
# Initialize services
|
||
log_service = LogCompressionService()
|
||
file_service = FileUploadService()
|
||
|
||
@logs_bp.route('/', methods=['POST'])
|
||
@logs_bp.route('/submit', methods=['POST'])
|
||
def submit_log():
|
||
"""
|
||
Enhanced log submission with compression support
|
||
|
||
Expected JSON:
|
||
{
|
||
"hostname": "device-01",
|
||
"device_ip": "192.168.1.100",
|
||
"nume_masa": "Masa-01",
|
||
"log_message": "Card detected: ABC123",
|
||
"severity": "info", # optional: debug, info, warning, error, critical
|
||
"source_file": "/path/to/logfile.log", # optional
|
||
"metadata": {} # optional additional metadata
|
||
}
|
||
"""
|
||
try:
|
||
# Validate content type
|
||
if not request.is_json:
|
||
return jsonify({
|
||
'error': 'Content-Type must be application/json',
|
||
'success': False
|
||
}), 400
|
||
|
||
data = request.get_json()
|
||
|
||
# Validate required fields
|
||
required_fields = ['hostname', 'device_ip', 'nume_masa', 'log_message']
|
||
missing_fields = [field for field in required_fields if not data.get(field)]
|
||
|
||
if missing_fields:
|
||
return jsonify({
|
||
'error': f'Missing required fields: {", ".join(missing_fields)}',
|
||
'success': False
|
||
}), 400
|
||
|
||
# Prepare device info
|
||
device_info = {
|
||
'hostname': data['hostname'],
|
||
'device_ip': data['device_ip'],
|
||
'nume_masa': data['nume_masa'],
|
||
# Optional – clients can send these to keep device records up to date
|
||
'device_type': data.get('device_type') or (data.get('metadata') or {}).get('device_type'),
|
||
'os_version': data.get('os_version') or (data.get('metadata') or {}).get('os_version'),
|
||
'location': data.get('location') or (data.get('metadata') or {}).get('location'),
|
||
'mac_address': data.get('mac_address') or (data.get('metadata') or {}).get('mac_address'),
|
||
}
|
||
|
||
# Process log with compression
|
||
result = log_service.process_log_message(
|
||
device_info=device_info,
|
||
message=data['log_message'],
|
||
severity=data.get('severity', 'info')
|
||
)
|
||
|
||
if result['success']:
|
||
# Prepare response with compression info
|
||
response_data = {
|
||
'success': True,
|
||
'message': 'Log processed successfully',
|
||
'log_id': result['log_id'],
|
||
'device_id': result['device_id'],
|
||
'compression_info': result['compression']
|
||
}
|
||
|
||
# Add alias info if template was used
|
||
if result['compression'].get('used_template'):
|
||
response_data['template_alias'] = result['compression']['template_alias']
|
||
|
||
# For clients: suggest using alias in future requests
|
||
if result['compression'].get('new_template'):
|
||
response_data['suggestion'] = f"For similar messages, you can use template alias: {result['compression']['template_alias']}"
|
||
|
||
return jsonify(response_data), 201
|
||
else:
|
||
return jsonify(result), 500
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error in submit_log: {e}")
|
||
return jsonify({
|
||
'error': 'Internal server error',
|
||
'success': False
|
||
}), 500
|
||
|
||
@logs_bp.route('/template/<alias>', methods=['POST'])
|
||
def submit_templated_log():
|
||
"""
|
||
Submit log using template alias (smaller payload)
|
||
|
||
Expected JSON:
|
||
{
|
||
"alias": "CD001",
|
||
"variables": {"card_id": "ABC123"},
|
||
"device_info": {
|
||
"hostname": "device-01",
|
||
"device_ip": "192.168.1.100",
|
||
"nume_masa": "Masa-01"
|
||
}
|
||
}
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
alias = request.view_args['alias']
|
||
|
||
# Validate required fields
|
||
if not data.get('device_info'):
|
||
return jsonify({
|
||
'error': 'device_info is required',
|
||
'success': False
|
||
}), 400
|
||
|
||
# Get template message
|
||
variables = data.get('variables', {})
|
||
full_message = log_service.get_message_by_alias(alias, variables)
|
||
|
||
if not full_message:
|
||
return jsonify({
|
||
'error': f'Template alias {alias} not found',
|
||
'success': False
|
||
}), 404
|
||
|
||
# Process as regular log
|
||
result = log_service.process_log_message(
|
||
device_info=data['device_info'],
|
||
message=full_message,
|
||
severity=data.get('severity', 'info')
|
||
)
|
||
|
||
return jsonify(result), 201 if result['success'] else 500
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error in submit_templated_log: {e}")
|
||
return jsonify({
|
||
'error': 'Internal server error',
|
||
'success': False
|
||
}), 500
|
||
|
||
@logs_bp.route('/file', methods=['POST'])
|
||
def upload_log_file():
|
||
"""
|
||
Upload log file for processing
|
||
|
||
Expects multipart/form-data with:
|
||
- file: log file
|
||
- device_info: JSON string with device information
|
||
"""
|
||
try:
|
||
# Check if file was uploaded
|
||
if 'file' not in request.files:
|
||
return jsonify({
|
||
'error': 'No file uploaded',
|
||
'success': False
|
||
}), 400
|
||
|
||
file = request.files['file']
|
||
if file.filename == '':
|
||
return jsonify({
|
||
'error': 'No file selected',
|
||
'success': False
|
||
}), 400
|
||
|
||
# Get device info
|
||
device_info_str = request.form.get('device_info')
|
||
if not device_info_str:
|
||
return jsonify({
|
||
'error': 'device_info is required',
|
||
'success': False
|
||
}), 400
|
||
|
||
import json
|
||
device_info = json.loads(device_info_str)
|
||
|
||
# Process file upload
|
||
result = file_service.process_uploaded_file(file, device_info)
|
||
|
||
return jsonify(result), 201 if result['success'] else 500
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error in upload_log_file: {e}")
|
||
return jsonify({
|
||
'error': 'Internal server error',
|
||
'success': False
|
||
}), 500
|
||
|
||
@logs_bp.route('/query', methods=['GET'])
|
||
def query_logs():
|
||
"""
|
||
Query logs with filters and pagination
|
||
|
||
Query parameters:
|
||
- device_id: Filter by device ID
|
||
- hostname: Filter by hostname
|
||
- severity: Filter by severity level
|
||
- start_time: Start time (ISO format)
|
||
- end_time: End time (ISO format)
|
||
- limit: Number of results (default 100)
|
||
- offset: Offset for pagination (default 0)
|
||
- include_template: Include resolved template messages (default true)
|
||
"""
|
||
try:
|
||
with get_db().get_session() as session:
|
||
# Build query
|
||
query = session.query(LogEntry).join(Device)
|
||
|
||
# Apply filters
|
||
if request.args.get('device_id'):
|
||
query = query.filter(LogEntry.device_id == int(request.args.get('device_id')))
|
||
|
||
if request.args.get('hostname'):
|
||
query = query.filter(Device.hostname == request.args.get('hostname'))
|
||
|
||
if request.args.get('severity'):
|
||
query = query.filter(LogEntry.severity == request.args.get('severity'))
|
||
|
||
if request.args.get('start_time'):
|
||
start_time = datetime.fromisoformat(request.args.get('start_time'))
|
||
query = query.filter(LogEntry.timestamp >= start_time)
|
||
|
||
if request.args.get('end_time'):
|
||
end_time = datetime.fromisoformat(request.args.get('end_time'))
|
||
query = query.filter(LogEntry.timestamp <= end_time)
|
||
|
||
# Pagination
|
||
limit = min(int(request.args.get('limit', 100)), 1000) # Max 1000
|
||
offset = int(request.args.get('offset', 0))
|
||
|
||
# Order by timestamp descending
|
||
query = query.order_by(LogEntry.timestamp.desc())
|
||
|
||
# Get total count
|
||
total_count = query.count()
|
||
|
||
# Apply pagination
|
||
logs = query.limit(limit).offset(offset).all()
|
||
|
||
# Format response
|
||
include_template = request.args.get('include_template', 'true').lower() == 'true'
|
||
log_data = []
|
||
|
||
for log in logs:
|
||
log_item = {
|
||
'id': log.id,
|
||
'device': {
|
||
'id': log.device.id,
|
||
'hostname': log.device.hostname,
|
||
'device_ip': log.device.device_ip,
|
||
'nume_masa': log.device.nume_masa
|
||
},
|
||
'timestamp': log.timestamp.isoformat(),
|
||
'severity': log.severity
|
||
}
|
||
|
||
if include_template and log.template:
|
||
log_item['message'] = log.resolved_message
|
||
log_item['template_alias'] = log.template.alias
|
||
log_item['template_category'] = log.template.category
|
||
else:
|
||
log_item['message'] = log.full_message or log.resolved_message
|
||
|
||
log_data.append(log_item)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'logs': log_data,
|
||
'pagination': {
|
||
'total_count': total_count,
|
||
'limit': limit,
|
||
'offset': offset,
|
||
'has_more': offset + limit < total_count
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error in query_logs: {e}")
|
||
return jsonify({
|
||
'error': 'Internal server error',
|
||
'success': False
|
||
}), 500
|
||
|
||
@logs_bp.route('/stats', methods=['GET'])
|
||
def get_log_stats():
|
||
"""Get logging and compression statistics"""
|
||
try:
|
||
# Get compression stats
|
||
compression_stats = log_service.get_compression_stats()
|
||
|
||
# Get additional stats
|
||
with get_db().get_session() as session:
|
||
# Device stats
|
||
active_devices = session.query(Device).filter_by(status='active').count()
|
||
total_devices = session.query(Device).count()
|
||
|
||
# Recent activity
|
||
from datetime import datetime, timedelta
|
||
last_hour = datetime.utcnow() - timedelta(hours=1)
|
||
recent_logs = session.query(LogEntry).filter(
|
||
LogEntry.timestamp >= last_hour
|
||
).count()
|
||
|
||
stats = {
|
||
'success': True,
|
||
'compression': compression_stats,
|
||
'devices': {
|
||
'active': active_devices,
|
||
'total': total_devices
|
||
},
|
||
'activity': {
|
||
'logs_last_hour': recent_logs
|
||
}
|
||
}
|
||
|
||
return jsonify(stats)
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error in get_log_stats: {e}")
|
||
return jsonify({
|
||
'error': 'Internal server error',
|
||
'success': False
|
||
}), 500
|
||
|
||
@logs_bp.route('/templates', methods=['GET'])
|
||
def get_templates():
|
||
"""Get available message templates and aliases"""
|
||
try:
|
||
with get_db().get_session() as session:
|
||
from app.models import MessageTemplate
|
||
|
||
templates = session.query(MessageTemplate).order_by(
|
||
MessageTemplate.usage_count.desc()
|
||
).all()
|
||
|
||
template_data = [{
|
||
'alias': template.alias,
|
||
'category': template.category,
|
||
'template_text': template.template_text,
|
||
'usage_count': template.usage_count,
|
||
'created_at': template.created_at.isoformat()
|
||
} for template in templates]
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'templates': template_data,
|
||
'total_count': len(template_data)
|
||
})
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error in get_templates: {e}")
|
||
return jsonify({
|
||
'error': 'Internal server error',
|
||
'success': False
|
||
}), 500 |