diff --git a/app.py b/app.py
index 2378017..bd27ef4 100644
--- a/app.py
+++ b/app.py
@@ -121,21 +121,51 @@ def display_haproxy_logs():
@app.route('/api/logs', methods=['POST'])
def api_get_logs():
+ """API endpoint for paginated and filtered logs"""
try:
log_file_path = '/var/log/haproxy.log'
if not os.path.exists(log_file_path):
- return jsonify({'error': 'Log file not found'}), 404
+ return jsonify({'error': 'Log file not found', 'success': False}), 404
page = request.json.get('page', 1)
per_page = request.json.get('per_page', 50)
+ search_query = request.json.get('search', '').lower()
+ exclude_phrases = request.json.get('exclude', [])
+
+ if page < 1:
+ page = 1
+ if per_page < 1 or per_page > 500:
+ per_page = 50
+
+ print(f"[API] page={page}, per_page={per_page}, search={search_query}, exclude={len(exclude_phrases)}", flush=True)
+
+ # Parse all logs
+ all_logs = parse_log_file(log_file_path)
+ total_logs = len(all_logs)
+
+ # Reverse to show newest first
+ all_logs = all_logs[::-1]
+
+ # Apply filters
+ filtered_logs = all_logs
+
+ if search_query:
+ filtered_logs = [log for log in filtered_logs if search_query in
+ f"{log.get('timestamp', '')} {log.get('ip_address', '')} {log.get('http_method', '')} {log.get('requested_url', '')}".lower()]
+
+ if exclude_phrases:
+ filtered_logs = [log for log in filtered_logs if not any(
+ phrase in f"{log.get('message', '')}" for phrase in exclude_phrases
+ )]
+
+ total_filtered = len(filtered_logs)
+
+ # Paginate
offset = (page - 1) * per_page
+ paginated_logs = filtered_logs[offset:offset + per_page]
- logs = parse_log_file(log_file_path)
- total_logs = len(logs)
-
- reversed_logs = logs[::-1]
- paginated_logs = reversed_logs[offset:offset + per_page]
+ print(f"[API] total={total_logs}, filtered={total_filtered}, returned={len(paginated_logs)}", flush=True)
return jsonify({
'success': True,
@@ -143,12 +173,15 @@ def api_get_logs():
'page': page,
'per_page': per_page,
'total': total_logs,
- 'has_more': offset + per_page < total_logs
+ 'total_filtered': total_filtered,
+ 'loaded_count': len(paginated_logs),
+ 'has_more': offset + per_page < total_filtered
})
except Exception as e:
print(f"[API] Error: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
+
@app.route('/home')
def home():
frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends()
diff --git a/static/js/logs.js b/static/js/logs.js
index ff6d761..1ce6256 100644
--- a/static/js/logs.js
+++ b/static/js/logs.js
@@ -27,7 +27,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Event Listeners
searchFilter.addEventListener('keyup', debounce(function() {
currentPage = 1;
- applyFilters();
+ loadLogsWithPage();
}, 300));
excludeBtn.addEventListener('click', function() {
@@ -36,7 +36,8 @@ document.addEventListener('DOMContentLoaded', function() {
if (!excludePhrases.includes(phrase)) {
excludePhrases.push(phrase);
updateExcludeUI();
- applyFilters();
+ currentPage = 1;
+ loadLogsWithPage();
}
excludeFilter.value = '';
}
@@ -52,7 +53,7 @@ document.addEventListener('DOMContentLoaded', function() {
excludeFilter.value = '';
updateExcludeUI();
currentPage = 1;
- applyFilters();
+ loadLogsWithPage();
});
perPageSelect.addEventListener('change', function() {
@@ -105,28 +106,48 @@ document.addEventListener('DOMContentLoaded', function() {
}
/**
- * Load logs from API
+ * Load initial logs from API
*/
function loadLogs() {
logsContainer.innerHTML = '
| Loading logs... |
';
+ loadLogsWithPage();
+ }
+
+ /**
+ * Load logs with pagination from API
+ */
+ function loadLogsWithPage() {
+ console.log(`[Logs] Loading page ${currentPage}, per_page ${perPage}`);
fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- page: 1,
- per_page: 200 // Load initial 200
+ page: currentPage,
+ per_page: perPage,
+ search: searchFilter.value.trim(),
+ exclude: excludePhrases
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
allLoadedLogs = data.logs;
- loadedSpan.textContent = data.logs.length;
+ loadedSpan.textContent = data.loaded_count;
totalLogs = data.total;
document.getElementById('total_count').textContent = data.total;
- console.log(`[Logs] Loaded ${data.logs.length}/${data.total}`, flush=true);
- applyFilters();
+
+ const totalPages = Math.ceil(data.total_filtered / perPage) || 1;
+ totalPagesSpan.textContent = totalPages;
+ matchSpan.textContent = data.total_filtered;
+ currentPageSpan.textContent = data.page;
+
+ renderLogs(data.logs);
+
+ prevBtn.disabled = currentPage === 1;
+ nextBtn.disabled = !data.has_more;
+
+ console.log(`[Logs] Page ${data.page}/${totalPages}, ${data.logs.length} logs`, flush=true);
} else {
showError(data.error);
}
@@ -138,7 +159,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
/**
- * Get filtered logs
+ * Get filtered logs (for local filtering)
*/
function getFilteredLogs() {
let filtered = allLoadedLogs;
@@ -164,23 +185,19 @@ document.addEventListener('DOMContentLoaded', function() {
}
/**
- * Apply all filters and render
+ * Apply local filters only
*/
function applyFilters() {
const filtered = getFilteredLogs();
- matchSpan.textContent = filtered.length;
+ renderLogs(filtered);
const totalPages = Math.ceil(filtered.length / perPage) || 1;
totalPagesSpan.textContent = totalPages;
currentPageSpan.textContent = currentPage;
-
- const offset = (currentPage - 1) * perPage;
- const paginated = filtered.slice(offset, offset + perPage);
-
- renderLogs(paginated);
+ matchSpan.textContent = filtered.length;
prevBtn.disabled = currentPage === 1;
- nextBtn.disabled = offset + perPage >= filtered.length;
+ nextBtn.disabled = (currentPage * perPage) >= filtered.length;
}
/**
@@ -188,7 +205,7 @@ document.addEventListener('DOMContentLoaded', function() {
*/
function renderLogs(logs) {
if (!logs || logs.length === 0) {
- logsContainer.innerHTML = '| No logs matching criteria |
';
+ logsContainer.innerHTML = '| No logs found |
';
return;
}
@@ -197,22 +214,35 @@ document.addEventListener('DOMContentLoaded', function() {
if (entry.xss_alert) threat_badges.push('XSS');
if (entry.sql_alert) threat_badges.push('SQL');
if (entry.webshell_alert) threat_badges.push('SHELL');
- if (entry.put_method) threat_badges.push('PUT');
+ if (entry.put_method) threat_badges.push('PUT');
if (entry.illegal_resource) threat_badges.push('403');
const threat_html = threat_badges.length > 0 ? `${threat_badges.join('')}
` : '';
+ let row_class = '';
+ if (entry.has_threat) {
+ row_class = 'table-danger';
+ } else if (entry.status_code.startsWith('5')) {
+ row_class = 'table-danger';
+ } else if (entry.status_code.startsWith('4')) {
+ row_class = 'table-warning';
+ } else if (entry.status_code.startsWith('2')) {
+ row_class = 'table-light';
+ } else {
+ row_class = 'table-light';
+ }
+
return `
-
+
${threat_html}
- ${escapeHtml(entry.timestamp)}
- ${escapeHtml(entry.ip_address)}
- ${escapeHtml(entry.http_method)}
- ${escapeHtml(entry.requested_url)}
- ${escapeHtml(entry.status_code)}
+ ${escapeHtml(entry.timestamp)}
+ ${escapeHtml(entry.ip_address)}
+ ${escapeHtml(entry.http_method)}
+ ${escapeHtml(entry.requested_url)}
+ ${escapeHtml(entry.status_code)}
- ${escapeHtml(entry.frontend)}~ ${escapeHtml(entry.backend)}
+ ${escapeHtml(entry.frontend)}~ ${escapeHtml(entry.backend)}
|
`;
@@ -251,7 +281,8 @@ document.addEventListener('DOMContentLoaded', function() {
window.removeExcludePhrase = function(idx) {
excludePhrases.splice(idx, 1);
updateExcludeUI();
- applyFilters();
+ currentPage = 1;
+ loadLogsWithPage();
};
/**