seller_central_backend/api/logs_views.py
vickytechkey ac9d0f0669
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 28s
feat: clear authentication classes for log views
2026-09-20 21:57:22 +05:30

296 lines
11 KiB
Python

import os
import re
from pathlib import Path
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework import status
ALLOWED_LOG_FILES = {
'app.log': 'Application Logs (INFO+)',
'error.log': 'Error Logs (ERROR+)',
'requests.log': 'HTTP Request/Response Logs',
'gunicorn-access.log': 'Gunicorn Access Logs',
'gunicorn-error.log': 'Gunicorn Error Logs',
}
LOG_LEVEL_REGEX = re.compile(r'\b(DEBUG|INFO|WARNING|WARN|ERROR|CRITICAL)\b', re.IGNORECASE)
TIMESTAMP_REGEX = re.compile(r'\[?(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]?')
def get_safe_log_path(filename):
"""Ensure requested file is strictly located inside settings.LOGS_DIR and in whitelist."""
if not filename:
filename = 'app.log'
# Security: basename only, no directory traversal
clean_filename = os.path.basename(filename)
if clean_filename not in ALLOWED_LOG_FILES and not clean_filename.endswith('.log'):
return None
full_path = Path(settings.LOGS_DIR).resolve() / clean_filename
# Prevent traversal
if not str(full_path).startswith(str(Path(settings.LOGS_DIR).resolve())):
return None
return full_path
def parse_log_line(line, line_num=0, source_file=''):
"""Extract structured details from a log line."""
clean_line = line.rstrip('\r\n')
level = 'INFO'
timestamp = ''
logger_name = ''
message = clean_line
ts_match = TIMESTAMP_REGEX.search(clean_line)
if ts_match:
timestamp = ts_match.group(1)
lvl_match = LOG_LEVEL_REGEX.search(clean_line)
if lvl_match:
raw_lvl = lvl_match.group(1).upper()
level = 'WARNING' if raw_lvl == 'WARN' else raw_lvl
# Try to parse pipe-separated format: [ts] LEVEL name ... | message
if ' | ' in clean_line:
parts = clean_line.split(' | ', 1)
header = parts[0]
message = parts[1]
header_parts = header.split()
for p in header_parts:
if p in ('api', 'django.request', 'django.security', 'root') or '.' in p:
logger_name = p
break
else:
# Fallback message
message = clean_line
return {
'id': line_num,
'source': source_file,
'timestamp': timestamp,
'level': level,
'logger': logger_name,
'message': message,
'raw': clean_line,
}
class LogsView(APIView):
"""
### 📊 Looking for the Interactive Log Dashboard?
Visit **[/dashboard/](/dashboard/)** for the full UI featuring real-time log streaming, level filters, keyword search, file selector, and API test runners.
"""
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request):
filename = request.query_params.get('file', 'app.log')
level_filter = request.query_params.get('level', 'ALL').upper()
search_query = request.query_params.get('search', '').strip()
tail = request.query_params.get('tail', 'true').lower() in ('true', '1')
raw_format = request.query_params.get('raw', 'false').lower() in ('true', '1')
try:
limit = int(request.query_params.get('limit', 150))
limit = max(1, min(limit, 2000))
except ValueError:
limit = 150
# Handle 'all' to read across multiple files
target_files = []
if filename == 'all':
target_files = ['app.log', 'error.log', 'requests.log']
else:
safe_path = get_safe_log_path(filename)
if not safe_path:
return Response({'error': f'Invalid or disallowed log file name: {filename}'}, status=status.HTTP_400_BAD_REQUEST)
target_files = [safe_path.name]
all_entries = []
line_counter = 0
file_stats = {}
level_counts = {'DEBUG': 0, 'INFO': 0, 'WARNING': 0, 'ERROR': 0, 'CRITICAL': 0}
for fname in target_files:
file_path = Path(settings.LOGS_DIR) / fname
if not file_path.exists():
file_stats[fname] = {'exists': False, 'size_bytes': 0, 'lines': 0}
continue
size_bytes = file_path.stat().st_size
file_stats[fname] = {
'exists': True,
'size_bytes': size_bytes,
'size_kb': round(size_bytes / 1024, 2),
}
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
file_stats[fname]['lines'] = len(lines)
for raw_line in lines:
if not raw_line.strip():
continue
line_counter += 1
parsed = parse_log_line(raw_line, line_num=line_counter, source_file=fname)
lvl = parsed['level']
if lvl in level_counts:
level_counts[lvl] += 1
all_entries.append(parsed)
except Exception as e:
return Response({'error': f'Failed reading {fname}: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Apply filtering
filtered = all_entries
# Level filter
if level_filter and level_filter != 'ALL':
filtered = [entry for entry in filtered if entry['level'] == level_filter]
# Search filter
if search_query:
q = search_query.lower()
filtered = [
entry for entry in filtered
if q in entry['raw'].lower() or q in entry['message'].lower() or q in entry['logger'].lower()
]
# Order & Limit
total_matched = len(filtered)
if tail:
# Take the most recent entries
display_entries = filtered[-limit:]
else:
display_entries = filtered[:limit]
if raw_format:
raw_text = '\n'.join([e['raw'] for e in display_entries])
return HttpResponse(raw_text, content_type='text/plain; charset=utf-8')
return Response({
'success': True,
'file': filename,
'available_files': list(ALLOWED_LOG_FILES.keys()),
'file_stats': file_stats,
'total_lines_read': line_counter,
'total_matched': total_matched,
'returned_count': len(display_entries),
'level_counts': level_counts,
'filters': {
'file': filename,
'level': level_filter,
'search': search_query,
'limit': limit,
'tail': tail,
},
'logs': display_entries,
})
class LogStatsView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request):
stats = {}
total_size = 0
total_errors = 0
total_warnings = 0
total_lines = 0
for filename, desc in ALLOWED_LOG_FILES.items():
file_path = Path(settings.LOGS_DIR) / filename
if not file_path.exists():
stats[filename] = {
'description': desc,
'exists': False,
'size_bytes': 0,
'size_formatted': '0 KB',
'line_count': 0,
'error_count': 0,
'last_modified': None,
}
continue
st = file_path.stat()
size = st.st_size
total_size += size
mtime = st.st_mtime
# Count lines and errors
lines_cnt = 0
err_cnt = 0
warn_cnt = 0
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
lines_cnt += 1
up = line.upper()
if 'ERROR' in up or 'CRITICAL' in up:
err_cnt += 1
elif 'WARN' in up:
warn_cnt += 1
except Exception:
pass
total_lines += lines_cnt
total_errors += err_cnt
total_warnings += warn_cnt
stats[filename] = {
'description': desc,
'exists': True,
'size_bytes': size,
'size_formatted': f"{round(size / 1024, 2)} KB" if size < 1024 * 1024 else f"{round(size / (1024 * 1024), 2)} MB",
'line_count': lines_cnt,
'error_count': err_cnt,
'warning_count': warn_cnt,
'last_modified': mtime,
}
return Response({
'files': stats,
'summary': {
'total_files': len(ALLOWED_LOG_FILES),
'total_lines': total_lines,
'total_errors': total_errors,
'total_warnings': total_warnings,
'total_size_bytes': total_size,
'total_size_formatted': f"{round(total_size / 1024, 2)} KB" if total_size < 1024 * 1024 else f"{round(total_size / (1024 * 1024), 2)} MB",
}
})
class LogClearView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def post(self, request):
filename = request.data.get('file', 'app.log')
if filename == 'all':
cleared = []
for fname in ALLOWED_LOG_FILES.keys():
file_path = Path(settings.LOGS_DIR) / fname
if file_path.exists():
with open(file_path, 'w', encoding='utf-8') as f:
f.write('')
cleared.append(fname)
return Response({'success': True, 'cleared': cleared, 'message': f'Cleared {len(cleared)} log files'})
safe_path = get_safe_log_path(filename)
if not safe_path:
return Response({'error': f'Invalid log file: {filename}'}, status=status.HTTP_400_BAD_REQUEST)
if not safe_path.exists():
return Response({'error': f'Log file does not exist: {filename}'}, status=status.HTTP_404_NOT_FOUND)
with open(safe_path, 'w', encoding='utf-8') as f:
f.write('')
return Response({'success': True, 'file': filename, 'message': f'Log file {filename} truncated successfully'})
def dashboard_view(request):
"""Serve the single-page testing and log viewer dashboard."""
return render(request, 'dashboard.html', {
'debug': settings.DEBUG,
'environment': os.environ.get('ENVIRONMENT', 'local'),
})