855 lines
29 KiB
HTML
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"> </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 %}
|
|
|