2025-08-12 13:33:25 +03:00

855 lines
29 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Log Viewer{% endblock %}
{% block css %}
<link href="{% static 'assets/plugins/highlight/styles/github.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item active">Log Viewer</li>
</ul>
<div class="row align-items-center mb-3">
<div class="col">
<h1 class="page-header">Log Viewer</h1>
<p class="text-muted">Monitor and analyze system logs in real-time</p>
</div>
<div class="col-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-primary" onclick="refreshLogs()">
<i class="fa fa-sync me-2"></i>Refresh
</button>
<button type="button" class="btn btn-outline-success" onclick="downloadLogs()">
<i class="fa fa-download me-2"></i>Download
</button>
<button type="button" class="btn btn-outline-info" onclick="clearLogs()">
<i class="fa fa-trash me-2"></i>Clear
</button>
</div>
</div>
</div>
<!-- Log Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Log Filters</h4>
<div class="card-tools">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetFilters()">
<i class="fa fa-undo me-1"></i>Reset
</button>
</div>
</div>
<div class="card-body">
<form id="logFilterForm" class="row g-3">
<div class="col-md-2">
<label class="form-label">Log Level</label>
<select id="logLevel" name="level" class="form-select">
<option value="">All Levels</option>
<option value="DEBUG">Debug</option>
<option value="INFO">Info</option>
<option value="WARNING">Warning</option>
<option value="ERROR">Error</option>
<option value="CRITICAL">Critical</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Log Source</label>
<select id="logSource" name="source" class="form-select">
<option value="">All Sources</option>
<option value="django">Django</option>
<option value="django.request">Django Requests</option>
<option value="django.db">Database</option>
<option value="django.security">Security</option>
<option value="celery">Celery</option>
<option value="gunicorn">Gunicorn</option>
<option value="nginx">Nginx</option>
<option value="application">Application</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Time Range</label>
<select id="timeRange" name="time_range" class="form-select">
<option value="1h">Last Hour</option>
<option value="6h">Last 6 Hours</option>
<option value="24h" selected>Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Search Text</label>
<div class="input-group">
<input type="text" id="searchText" name="search" class="form-control" placeholder="Search in logs...">
<button type="button" class="btn btn-outline-primary" onclick="applyFilters()">
<i class="fa fa-search"></i>
</button>
</div>
</div>
<div class="col-md-2">
<label class="form-label">Lines</label>
<select id="maxLines" name="max_lines" class="form-select">
<option value="100">100 lines</option>
<option value="500" selected>500 lines</option>
<option value="1000">1000 lines</option>
<option value="5000">5000 lines</option>
</select>
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="autoRefresh" checked>
<label class="form-check-label" for="autoRefresh">
Auto
</label>
</div>
</div>
</form>
<!-- Custom Date Range -->
<div id="customDateRange" class="row mt-3" style="display: none;">
<div class="col-md-3">
<label class="form-label">Start Date</label>
<input type="datetime-local" id="startDate" name="start_date" class="form-control">
</div>
<div class="col-md-3">
<label class="form-label">End Date</label>
<input type="datetime-local" id="endDate" name="end_date" class="form-control">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Log Statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<div class="fs-24px fw-600 text-primary" id="totalLogsCount">0</div>
<h6 class="text-muted">Total Logs</h6>
<p class="text-muted small mb-0">In selected range</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<div class="fs-24px fw-600 text-danger" id="errorLogsCount">0</div>
<h6 class="text-muted">Errors</h6>
<p class="text-muted small mb-0">Error + Critical</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<div class="fs-24px fw-600 text-warning" id="warningLogsCount">0</div>
<h6 class="text-muted">Warnings</h6>
<p class="text-muted small mb-0">Warning level</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<div class="fs-24px fw-600 text-success" id="infoLogsCount">0</div>
<h6 class="text-muted">Info</h6>
<p class="text-muted small mb-0">Info + Debug</p>
</div>
</div>
</div>
</div>
<!-- Log Content -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Log Content</h4>
<div class="card-tools">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="toggleWrap()">
<i class="fa fa-text-width me-1"></i>Wrap
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleTimestamps()">
<i class="fa fa-clock me-1"></i>Timestamps
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleHighlight()">
<i class="fa fa-highlighter me-1"></i>Highlight
</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div id="logContainer" class="log-container">
<div id="logContent" class="log-content">
<div class="text-center p-4 text-muted">
<i class="fa fa-file-alt fa-3x mb-3"></i>
<p>Loading logs...</p>
</div>
</div>
</div>
<!-- Log Loading Indicator -->
<div id="logLoading" class="text-center p-3" style="display: none;">
<div class="spinner-border spinner-border-sm me-2"></div>
Loading more logs...
</div>
</div>
<div class="card-footer">
<div class="row align-items-center">
<div class="col">
<small class="text-muted">
<span id="logStatus">Ready</span> |
Last updated: <span id="lastUpdated">Never</span>
</small>
</div>
<div class="col-auto">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary" onclick="scrollToTop()">
<i class="fa fa-arrow-up"></i> Top
</button>
<button type="button" class="btn btn-outline-primary" onclick="scrollToBottom()">
<i class="fa fa-arrow-down"></i> Bottom
</button>
<button type="button" class="btn btn-outline-secondary" onclick="loadMoreLogs()">
<i class="fa fa-plus"></i> Load More
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Log Entry Detail Modal -->
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Log Entry Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="logDetailContent">
<!-- Log details will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="copyLogEntry()">
<i class="fa fa-copy me-2"></i>Copy
</button>
</div>
</div>
</div>
</div>
<!-- Export Logs Modal -->
<div class="modal fade" id="exportLogsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Export Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="exportLogsForm">
<div class="modal-body">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Export Format</label>
<select name="format" class="form-select" required>
<option value="txt">Plain Text (.txt)</option>
<option value="csv">CSV (.csv)</option>
<option value="json">JSON (.json)</option>
<option value="xlsx">Excel (.xlsx)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Include Filters</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="include_filters" checked>
<label class="form-check-label">
Apply current filters to export
</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Max Records</label>
<select name="max_records" class="form-select">
<option value="1000">1,000 records</option>
<option value="5000">5,000 records</option>
<option value="10000">10,000 records</option>
<option value="50000">50,000 records</option>
<option value="all">All records</option>
</select>
</div>
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i>
Large exports may take some time to process. You'll receive a download link when ready.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Export Logs</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'assets/plugins/highlight/highlight.min.js' %}"></script>
<script>
var autoRefreshInterval;
var currentLogData = [];
var logSettings = {
wrapLines: false,
showTimestamps: true,
highlightSyntax: true
};
$(document).ready(function() {
loadLogs();
setupEventHandlers();
startAutoRefresh();
// Initialize highlight.js
hljs.highlightAll();
});
function setupEventHandlers() {
// Filter changes
$('#logLevel, #logSource, #timeRange, #maxLines').change(function() {
applyFilters();
});
// Search on Enter
$('#searchText').keypress(function(e) {
if (e.which === 13) {
applyFilters();
}
});
// Time range custom option
$('#timeRange').change(function() {
if ($(this).val() === 'custom') {
$('#customDateRange').show();
} else {
$('#customDateRange').hide();
}
});
// Auto refresh toggle
$('#autoRefresh').change(function() {
if ($(this).is(':checked')) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// Export form
$('#exportLogsForm').submit(function(e) {
e.preventDefault();
exportLogs();
});
// Scroll events for infinite loading
$('#logContainer').scroll(function() {
var container = $(this);
if (container.scrollTop() + container.innerHeight() >= container[0].scrollHeight - 100) {
loadMoreLogs();
}
});
}
function loadLogs() {
$('#logStatus').text('Loading...');
var filters = getFilters();
$.get('{% url "core:get_logs" %}', filters, function(data) {
currentLogData = data.logs;
updateLogStats(data.stats);
renderLogs(data.logs);
$('#logStatus').text('Ready');
$('#lastUpdated').text(new Date().toLocaleTimeString());
}).fail(function() {
$('#logContent').html('<div class="alert alert-danger">Failed to load logs</div>');
$('#logStatus').text('Error');
});
}
function getFilters() {
var filters = {
level: $('#logLevel').val(),
source: $('#logSource').val(),
time_range: $('#timeRange').val(),
search: $('#searchText').val(),
max_lines: $('#maxLines').val()
};
if (filters.time_range === 'custom') {
filters.start_date = $('#startDate').val();
filters.end_date = $('#endDate').val();
}
return filters;
}
function updateLogStats(stats) {
$('#totalLogsCount').text(stats.total.toLocaleString());
$('#errorLogsCount').text(stats.errors.toLocaleString());
$('#warningLogsCount').text(stats.warnings.toLocaleString());
$('#infoLogsCount').text(stats.info.toLocaleString());
}
function renderLogs(logs) {
var html = '';
if (logs.length === 0) {
html = '<div class="text-center p-4 text-muted">' +
'<i class="fa fa-search fa-3x mb-3"></i>' +
'<p>No logs found matching the current filters</p>' +
'</div>';
} else {
logs.forEach(function(log, index) {
html += renderLogEntry(log, index);
});
}
$('#logContent').html(html);
// Apply syntax highlighting if enabled
if (logSettings.highlightSyntax) {
$('#logContent pre code').each(function(i, block) {
hljs.highlightElement(block);
});
}
}
function renderLogEntry(log, index) {
var levelClass = getLevelClass(log.level);
var timestamp = logSettings.showTimestamps ?
'<span class="log-timestamp">' + formatTimestamp(log.timestamp) + '</span>' : '';
var wrapClass = logSettings.wrapLines ? 'log-wrap' : 'log-nowrap';
return '<div class="log-entry ' + levelClass + '" data-index="' + index + '" onclick="showLogDetail(' + index + ')">' +
'<div class="log-header">' +
'<span class="log-level badge bg-' + getLevelColor(log.level) + '">' + log.level + '</span>' +
'<span class="log-source">' + log.source + '</span>' +
timestamp +
'</div>' +
'<div class="log-message ' + wrapClass + '">' +
'<pre><code>' + escapeHtml(log.message) + '</code></pre>' +
'</div>' +
(log.traceback ? '<div class="log-traceback"><pre><code>' + escapeHtml(log.traceback) + '</code></pre></div>' : '') +
'</div>';
}
function getLevelClass(level) {
switch (level) {
case 'DEBUG': return 'log-debug';
case 'INFO': return 'log-info';
case 'WARNING': return 'log-warning';
case 'ERROR': return 'log-error';
case 'CRITICAL': return 'log-critical';
default: return 'log-info';
}
}
function getLevelColor(level) {
switch (level) {
case 'DEBUG': return 'secondary';
case 'INFO': return 'primary';
case 'WARNING': return 'warning';
case 'ERROR': return 'danger';
case 'CRITICAL': return 'dark';
default: return 'primary';
}
}
function formatTimestamp(timestamp) {
return new Date(timestamp).toLocaleString();
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showLogDetail(index) {
var log = currentLogData[index];
var detailHtml = '<div class="row">' +
'<div class="col-md-6">' +
'<h6>Log Information</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Level:</strong></td><td><span class="badge bg-' + getLevelColor(log.level) + '">' + log.level + '</span></td></tr>' +
'<tr><td><strong>Source:</strong></td><td>' + log.source + '</td></tr>' +
'<tr><td><strong>Timestamp:</strong></td><td>' + formatTimestamp(log.timestamp) + '</td></tr>' +
'<tr><td><strong>Thread:</strong></td><td>' + (log.thread || 'N/A') + '</td></tr>' +
'<tr><td><strong>Process:</strong></td><td>' + (log.process || 'N/A') + '</td></tr>' +
'</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6>Context</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>File:</strong></td><td>' + (log.filename || 'N/A') + '</td></tr>' +
'<tr><td><strong>Line:</strong></td><td>' + (log.lineno || 'N/A') + '</td></tr>' +
'<tr><td><strong>Function:</strong></td><td>' + (log.funcName || 'N/A') + '</td></tr>' +
'<tr><td><strong>Module:</strong></td><td>' + (log.module || 'N/A') + '</td></tr>' +
'</table>' +
'</div>' +
'</div>' +
'<hr>' +
'<h6>Message</h6>' +
'<pre class="bg-light p-3"><code>' + escapeHtml(log.message) + '</code></pre>';
if (log.traceback) {
detailHtml += '<hr><h6>Traceback</h6>' +
'<pre class="bg-light p-3"><code>' + escapeHtml(log.traceback) + '</code></pre>';
}
if (log.extra) {
detailHtml += '<hr><h6>Extra Data</h6>' +
'<pre class="bg-light p-3"><code>' + JSON.stringify(log.extra, null, 2) + '</code></pre>';
}
$('#logDetailContent').html(detailHtml);
$('#logDetailModal').modal('show');
}
function applyFilters() {
loadLogs();
}
function resetFilters() {
$('#logFilterForm')[0].reset();
$('#customDateRange').hide();
loadLogs();
}
function refreshLogs() {
loadLogs();
toastr.success('Logs refreshed');
}
function startAutoRefresh() {
stopAutoRefresh();
autoRefreshInterval = setInterval(function() {
if ($('#autoRefresh').is(':checked')) {
loadLogs();
}
}, 30000); // Refresh every 30 seconds
}
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
}
function loadMoreLogs() {
if ($('#logLoading').is(':visible')) return;
$('#logLoading').show();
var filters = getFilters();
filters.offset = currentLogData.length;
$.get('{% url "core:get_logs" %}', filters, function(data) {
currentLogData = currentLogData.concat(data.logs);
var newLogsHtml = '';
data.logs.forEach(function(log, index) {
newLogsHtml += renderLogEntry(log, currentLogData.length - data.logs.length + index);
});
$('#logContent').append(newLogsHtml);
$('#logLoading').hide();
// Apply syntax highlighting to new content
if (logSettings.highlightSyntax) {
$('#logContent pre code').each(function(i, block) {
if (!$(block).hasClass('hljs')) {
hljs.highlightElement(block);
}
});
}
}).fail(function() {
$('#logLoading').hide();
toastr.error('Failed to load more logs');
});
}
function scrollToTop() {
$('#logContainer').scrollTop(0);
}
function scrollToBottom() {
var container = $('#logContainer');
container.scrollTop(container[0].scrollHeight);
}
function toggleWrap() {
logSettings.wrapLines = !logSettings.wrapLines;
$('.log-message').toggleClass('log-wrap log-nowrap');
}
function toggleTimestamps() {
logSettings.showTimestamps = !logSettings.showTimestamps;
$('.log-timestamp').toggle();
}
function toggleHighlight() {
logSettings.highlightSyntax = !logSettings.highlightSyntax;
if (logSettings.highlightSyntax) {
$('#logContent pre code').each(function(i, block) {
hljs.highlightElement(block);
});
} else {
$('#logContent pre code').removeClass('hljs').removeAttr('data-highlighted');
}
}
function downloadLogs() {
$('#exportLogsModal').modal('show');
}
function exportLogs() {
var formData = new FormData($('#exportLogsForm')[0]);
// Add current filters if requested
if (formData.get('include_filters')) {
var filters = getFilters();
Object.keys(filters).forEach(function(key) {
if (filters[key]) {
formData.append('filter_' + key, filters[key]);
}
});
}
$.post('{% url "core:export_logs" %}', formData, function(data) {
if (data.success) {
$('#exportLogsModal').modal('hide');
toastr.success('Export started. Download link will be sent to your email.');
} else {
toastr.error('Export failed: ' + data.error);
}
}).fail(function() {
toastr.error('Export failed');
});
}
function clearLogs() {
if (confirm('Are you sure you want to clear all logs? This action cannot be undone.')) {
$.post('{% url "core:clear_logs" %}', function(data) {
if (data.success) {
loadLogs();
toastr.success('Logs cleared successfully');
} else {
toastr.error('Failed to clear logs: ' + data.error);
}
}).fail(function() {
toastr.error('Failed to clear logs');
});
}
}
function copyLogEntry() {
var content = $('#logDetailContent').text();
navigator.clipboard.writeText(content).then(function() {
toastr.success('Log entry copied to clipboard');
}).catch(function() {
toastr.error('Failed to copy to clipboard');
});
}
</script>
<style>
.fs-24px {
font-size: 24px;
}
.fw-600 {
font-weight: 600;
}
.card-tools {
margin-left: auto;
}
.log-container {
height: 600px;
overflow-y: auto;
background-color: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.log-content {
padding: 1rem;
}
.log-entry {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-left: 3px solid transparent;
cursor: pointer;
transition: background-color 0.2s ease;
}
.log-entry:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.log-debug {
border-left-color: #6c757d;
}
.log-info {
border-left-color: #0d6efd;
}
.log-warning {
border-left-color: #ffc107;
}
.log-error {
border-left-color: #dc3545;
}
.log-critical {
border-left-color: #6f42c1;
background-color: rgba(220, 53, 69, 0.1);
}
.log-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.875rem;
}
.log-level {
font-size: 0.75rem;
}
.log-source {
color: #ffc107;
font-weight: 500;
}
.log-timestamp {
color: #6c757d;
margin-left: auto;
}
.log-message {
margin: 0;
}
.log-message pre {
margin: 0;
background: none;
border: none;
padding: 0;
color: inherit;
font-size: 0.875rem;
line-height: 1.4;
}
.log-wrap pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.log-nowrap pre {
white-space: pre;
overflow-x: auto;
}
.log-traceback {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: rgba(220, 53, 69, 0.1);
border-radius: 0.25rem;
}
.log-traceback pre {
margin: 0;
color: #ff6b6b;
font-size: 0.8rem;
}
/* Syntax highlighting adjustments for dark theme */
.hljs {
background: none !important;
}
.hljs-string {
color: #ce9178;
}
.hljs-number {
color: #b5cea8;
}
.hljs-keyword {
color: #569cd6;
}
.hljs-comment {
color: #6a9955;
}
/* Scrollbar styling */
.log-container::-webkit-scrollbar {
width: 8px;
}
.log-container::-webkit-scrollbar-track {
background: #2d2d30;
}
.log-container::-webkit-scrollbar-thumb {
background: #464647;
border-radius: 4px;
}
.log-container::-webkit-scrollbar-thumb:hover {
background: #5a5a5c;
}
</style>
{% endblock %}