Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
341
resources/views/admin/database/dashboard.blade.php
Normal file
341
resources/views/admin/database/dashboard.blade.php
Normal file
@@ -0,0 +1,341 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Database Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">Database Dashboard</h1>
|
||||
|
||||
<div class="row">
|
||||
<!-- Connection Selector -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Database Connection</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<select id="connection-selector" class="form-control">
|
||||
@foreach($connections as $conn)
|
||||
<option value="{{ $conn }}" {{ $conn === $activeConnection ? 'selected' : '' }}>
|
||||
{{ $conn }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Connection Stats -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Connection Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="connection-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Driver</th>
|
||||
<td>{{ $stats['driver'] ?? 'Unknown' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<td>{{ $stats['version'] ?? 'Unknown' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Connection Status</th>
|
||||
<td>{{ $stats['connection_status'] ?? 'Unknown' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tables</th>
|
||||
<td>{{ $stats['table_count'] ?? 'Unknown' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Indexes</th>
|
||||
<td>{{ $stats['index_count'] ?? 'Unknown' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Rows (approx.)</th>
|
||||
<td>{{ number_format($stats['total_rows'] ?? 0) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Stats -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Query Statistics</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="query-stats">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Total Queries</th>
|
||||
<td id="total-queries">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Query Time</th>
|
||||
<td id="total-time">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Average Query Time</th>
|
||||
<td id="average-time">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Slow Queries</th>
|
||||
<td id="slow-queries">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Recent Queries -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Recent Queries</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="recent-queries">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SQL</th>
|
||||
<th>Parameters</th>
|
||||
<th>Time (ms)</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Slow Queries -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Slow Queries</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="slow-queries-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SQL</th>
|
||||
<th>Parameters</th>
|
||||
<th>Time (ms)</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Connection</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Database-Specific Stats -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Database-Specific Statistics</h6>
|
||||
</div>
|
||||
<div class="card-body" id="specific-stats">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Load initial data
|
||||
loadQueryStats();
|
||||
loadSpecificStats('{{ $activeConnection }}');
|
||||
|
||||
// Set up connection selector
|
||||
$('#connection-selector').change(function() {
|
||||
const connection = $(this).val();
|
||||
window.location.href = '/admin/database/dashboard?connection=' + connection;
|
||||
});
|
||||
|
||||
// Auto-refresh data every 10 seconds
|
||||
setInterval(function() {
|
||||
loadQueryStats();
|
||||
loadSpecificStats('{{ $activeConnection }}');
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
function loadQueryStats() {
|
||||
$.ajax({
|
||||
url: '/admin/database/queries',
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
// Update query stats
|
||||
$('#total-queries').text(data.total_queries);
|
||||
$('#total-time').text(data.total_time.toFixed(2) + ' ms');
|
||||
$('#average-time').text(data.average_time.toFixed(2) + ' ms');
|
||||
$('#slow-queries').text(data.slow_queries);
|
||||
|
||||
// Update recent queries table
|
||||
const recentQueriesHtml = data.recent_queries.map(query => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(query.sql)}</code></td>
|
||||
<td><code>${JSON.stringify(query.parameters)}</code></td>
|
||||
<td>${query.time.toFixed(2)} ms</td>
|
||||
<td>${new Date(query.timestamp * 1000).toLocaleString()}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
$('#recent-queries tbody').html(recentQueriesHtml || '<tr><td colspan="4" class="text-center">No queries found</td></tr>');
|
||||
|
||||
// Update slow queries table
|
||||
const slowQueriesHtml = data.slow_query_list.map(query => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(query.sql)}</code></td>
|
||||
<td><code>${JSON.stringify(query.parameters)}</code></td>
|
||||
<td>${query.time.toFixed(2)} ms</td>
|
||||
<td>${new Date(query.timestamp * 1000).toLocaleString()}</td>
|
||||
<td>${query.connection}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
$('#slow-queries-table tbody').html(slowQueriesHtml || '<tr><td colspan="5" class="text-center">No slow queries found</td></tr>');
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load query statistics');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadSpecificStats(connection) {
|
||||
$.ajax({
|
||||
url: '/admin/database/specific/' + connection,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
let html = '';
|
||||
|
||||
// MySQL-specific stats
|
||||
if (data.query_cache) {
|
||||
html += '<h5>Query Cache</h5>';
|
||||
html += '<div class="table-responsive"><table class="table table-bordered">';
|
||||
html += '<tbody>';
|
||||
for (const [key, value] of Object.entries(data.query_cache)) {
|
||||
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
if (data.buffer_pool) {
|
||||
html += '<h5>Buffer Pool</h5>';
|
||||
html += '<div class="table-responsive"><table class="table table-bordered">';
|
||||
html += '<tbody>';
|
||||
for (const [key, value] of Object.entries(data.buffer_pool)) {
|
||||
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
// PostgreSQL-specific stats
|
||||
if (data.cache) {
|
||||
html += '<h5>Cache Statistics</h5>';
|
||||
html += '<div class="table-responsive"><table class="table table-bordered">';
|
||||
html += '<tbody>';
|
||||
for (const [key, value] of Object.entries(data.cache)) {
|
||||
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
if (data.long_running && data.long_running.length > 0) {
|
||||
html += '<h5>Long-Running Queries</h5>';
|
||||
html += '<div class="table-responsive"><table class="table table-bordered">';
|
||||
html += '<thead><tr><th>Query</th><th>Duration</th><th>User</th></tr></thead>';
|
||||
html += '<tbody>';
|
||||
for (const query of data.long_running) {
|
||||
html += `<tr>
|
||||
<td><code>${escapeHtml(query.query)}</code></td>
|
||||
<td>${query.duration.toFixed(2)} seconds</td>
|
||||
<td>${query.usename}</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
// SQLite-specific stats
|
||||
if (data.database) {
|
||||
html += '<h5>Database Statistics</h5>';
|
||||
html += '<div class="table-responsive"><table class="table table-bordered">';
|
||||
html += '<tbody>';
|
||||
for (const [key, value] of Object.entries(data.database)) {
|
||||
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
if (data.pragmas) {
|
||||
html += '<h5>PRAGMA Settings</h5>';
|
||||
html += '<div class="table-responsive"><table class="table table-bordered">';
|
||||
html += '<tbody>';
|
||||
for (const [key, value] of Object.entries(data.pragmas)) {
|
||||
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
$('#specific-stats').html(html || '<div class="text-center">No specific statistics available</div>');
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load database-specific statistics');
|
||||
$('#specific-stats').html('<div class="alert alert-danger">Failed to load database-specific statistics</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
293
resources/views/admin/database/health.blade.php
Normal file
293
resources/views/admin/database/health.blade.php
Normal file
@@ -0,0 +1,293 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Database Health Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">Database Health Dashboard</h1>
|
||||
|
||||
<div class="row">
|
||||
<!-- Connection Selector -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Database Connection</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<select id="connection-selector" class="form-control">
|
||||
<option value="all">All Connections</option>
|
||||
@foreach($connections as $conn)
|
||||
<option value="{{ $conn }}">{{ $conn }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button id="refresh-button" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
<div class="float-right">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="auto-refresh">
|
||||
<label class="custom-control-label" for="auto-refresh">Auto-refresh (30s)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="health-results">
|
||||
<!-- Health check results will be loaded here -->
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Check Details Modal -->
|
||||
<div class="modal fade" id="health-details-modal" tabindex="-1" role="dialog" aria-labelledby="health-details-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="health-details-modal-label">Health Check Details</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="health-details-content">
|
||||
<!-- Health check details will be loaded here -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
// Global variables
|
||||
let autoRefreshInterval = null;
|
||||
let currentConnection = 'all';
|
||||
|
||||
$(document).ready(function() {
|
||||
// Load initial data
|
||||
loadHealthData();
|
||||
|
||||
// Set up connection selector
|
||||
$('#connection-selector').change(function() {
|
||||
currentConnection = $(this).val();
|
||||
loadHealthData();
|
||||
});
|
||||
|
||||
// Set up refresh button
|
||||
$('#refresh-button').click(function() {
|
||||
loadHealthData();
|
||||
});
|
||||
|
||||
// Set up auto-refresh
|
||||
$('#auto-refresh').change(function() {
|
||||
if ($(this).is(':checked')) {
|
||||
autoRefreshInterval = setInterval(loadHealthData, 30000);
|
||||
} else {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadHealthData() {
|
||||
const url = currentConnection === 'all'
|
||||
? '/admin/database/health/all'
|
||||
: `/admin/database/health/${currentConnection}`;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
renderHealthData(data);
|
||||
},
|
||||
error: function() {
|
||||
$('#health-results').html('<div class="alert alert-danger">Failed to load health data</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderHealthData(data) {
|
||||
let html = '';
|
||||
|
||||
if (currentConnection === 'all') {
|
||||
// Render all connections
|
||||
let allHealthy = true;
|
||||
|
||||
for (const [connection, result] of Object.entries(data)) {
|
||||
if (!result.is_healthy) {
|
||||
allHealthy = false;
|
||||
}
|
||||
|
||||
html += renderConnectionHealth(connection, result);
|
||||
}
|
||||
|
||||
// Add overall status card
|
||||
const overallStatusHtml = `
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card ${allHealthy ? 'bg-success' : 'bg-danger'} text-white shadow">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-1 text-center">
|
||||
<i class="fas ${allHealthy ? 'fa-check-circle' : 'fa-exclamation-circle'} fa-3x"></i>
|
||||
</div>
|
||||
<div class="col-md-11">
|
||||
<h5>Overall Status: ${allHealthy ? 'Healthy' : 'Unhealthy'}</h5>
|
||||
<p class="mb-0">${allHealthy ? 'All database connections are healthy' : 'One or more database connections have issues'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
html = overallStatusHtml + html;
|
||||
} else {
|
||||
// Render single connection
|
||||
html = renderConnectionHealth(currentConnection, data);
|
||||
}
|
||||
|
||||
$('#health-results').html(html);
|
||||
|
||||
// Set up view details buttons
|
||||
$('.view-details').click(function() {
|
||||
const checkId = $(this).data('check-id');
|
||||
const connection = $(this).data('connection');
|
||||
const checkData = $(this).data('check');
|
||||
|
||||
$('#health-details-modal-label').text(`${checkData.name} Details`);
|
||||
|
||||
let detailsHtml = `
|
||||
<div class="mb-3">
|
||||
<h6>Status</h6>
|
||||
<p>${formatStatus(checkData.status)}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<h6>Message</h6>
|
||||
<p>${checkData.message}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<h6>Description</h6>
|
||||
<p>${checkData.description}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#health-details-content').html(detailsHtml);
|
||||
$('#health-details-modal').modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
function renderConnectionHealth(connection, result) {
|
||||
const timestamp = result.timestamp;
|
||||
const overallStatus = result.overall_status;
|
||||
const isHealthy = result.is_healthy;
|
||||
|
||||
let html = `
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Connection: ${connection}</h6>
|
||||
<div>
|
||||
<span class="mr-2">Status: ${formatStatus(overallStatus)}</span>
|
||||
<span>Last checked: ${timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
`;
|
||||
|
||||
// Display errors
|
||||
if (result.errors && Object.keys(result.errors).length > 0) {
|
||||
html += `
|
||||
<div class="alert alert-danger">
|
||||
<h5>Errors</h5>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
for (const [errorId, error] of Object.entries(result.errors)) {
|
||||
html += `<li><strong>${error.name}:</strong> ${error.message}</li>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Display checks
|
||||
html += `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Check</th>
|
||||
<th>Status</th>
|
||||
<th>Message</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
for (const [checkId, check] of Object.entries(result.checks)) {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${check.name}</td>
|
||||
<td>${formatStatus(check.status)}</td>
|
||||
<td>${check.message}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info view-details"
|
||||
data-check-id="${checkId}"
|
||||
data-connection="${connection}"
|
||||
data-check='${JSON.stringify(check)}'>
|
||||
View Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatStatus(status) {
|
||||
switch (status) {
|
||||
case 'ok':
|
||||
return '<span class="badge badge-success">OK</span>';
|
||||
case 'warning':
|
||||
return '<span class="badge badge-warning">WARNING</span>';
|
||||
case 'error':
|
||||
return '<span class="badge badge-danger">ERROR</span>';
|
||||
default:
|
||||
return `<span class="badge badge-secondary">${status}</span>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
496
resources/views/admin/database/history.blade.php
Normal file
496
resources/views/admin/database/history.blade.php
Normal file
@@ -0,0 +1,496 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Database Query History')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">Database Query History</h1>
|
||||
|
||||
<div class="row">
|
||||
<!-- Date Range Selector -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Date Range</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="start-date">Start Date</label>
|
||||
<input type="date" id="start-date" class="form-control" value="{{ $startDate }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="end-date">End Date</label>
|
||||
<input type="date" id="end-date" class="form-control" value="{{ $endDate }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label> </label>
|
||||
<button id="update-range" class="btn btn-primary btn-block">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-12">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-secondary" data-range="today">Today</button>
|
||||
<button type="button" class="btn btn-secondary" data-range="yesterday">Yesterday</button>
|
||||
<button type="button" class="btn btn-secondary" data-range="last7days">Last 7 Days</button>
|
||||
<button type="button" class="btn btn-secondary" data-range="last30days">Last 30 Days</button>
|
||||
<button type="button" class="btn btn-secondary" data-range="thismonth">This Month</button>
|
||||
<button type="button" class="btn btn-secondary" data-range="lastmonth">Last Month</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Daily Query Statistics -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Daily Query Statistics</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container" style="position: relative; height:300px;">
|
||||
<canvas id="daily-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Hourly Query Statistics -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Hourly Query Statistics</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container" style="position: relative; height:300px;">
|
||||
<canvas id="hourly-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Top Slow Queries -->
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Top Slow Queries</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="slow-queries-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SQL</th>
|
||||
<th>Executions</th>
|
||||
<th>Avg Time (ms)</th>
|
||||
<th>Max Time (ms)</th>
|
||||
<th>Min Time (ms)</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Details Modal -->
|
||||
<div class="modal fade" id="query-details-modal" tabindex="-1" role="dialog" aria-labelledby="query-details-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="query-details-modal-label">Query Details</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>SQL</h6>
|
||||
<pre id="query-details-sql" class="bg-light p-3 mb-4"></pre>
|
||||
|
||||
<h6>Execution Trend</h6>
|
||||
<div class="chart-container mb-4" style="position: relative; height:200px;">
|
||||
<canvas id="query-trend-chart"></canvas>
|
||||
</div>
|
||||
|
||||
<h6>Recent Executions</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="query-details-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameters</th>
|
||||
<th>Execution Time (ms)</th>
|
||||
<th>Connection</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
|
||||
<script>
|
||||
// Global chart objects
|
||||
let dailyChart = null;
|
||||
let hourlyChart = null;
|
||||
let queryTrendChart = null;
|
||||
|
||||
// Current date range
|
||||
let startDate = '{{ $startDate }}';
|
||||
let endDate = '{{ $endDate }}';
|
||||
|
||||
// Current query hash for details
|
||||
let currentQueryHash = '';
|
||||
|
||||
$(document).ready(function() {
|
||||
// Load initial data
|
||||
loadDailyStats();
|
||||
loadHourlyStats();
|
||||
loadSlowQueries();
|
||||
|
||||
// Set up date range selector
|
||||
$('#update-range').click(function() {
|
||||
startDate = $('#start-date').val();
|
||||
endDate = $('#end-date').val();
|
||||
|
||||
loadDailyStats();
|
||||
loadHourlyStats();
|
||||
loadSlowQueries();
|
||||
});
|
||||
|
||||
// Set up quick date range buttons
|
||||
$('.btn-group button').click(function() {
|
||||
const range = $(this).data('range');
|
||||
const dates = getDateRange(range);
|
||||
|
||||
$('#start-date').val(dates.start);
|
||||
$('#end-date').val(dates.end);
|
||||
|
||||
startDate = dates.start;
|
||||
endDate = dates.end;
|
||||
|
||||
loadDailyStats();
|
||||
loadHourlyStats();
|
||||
loadSlowQueries();
|
||||
});
|
||||
});
|
||||
|
||||
function loadDailyStats() {
|
||||
$.ajax({
|
||||
url: `/admin/database/history/daily?startDate=${startDate}&endDate=${endDate}`,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
renderDailyChart(data);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load daily statistics');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadHourlyStats() {
|
||||
$.ajax({
|
||||
url: `/admin/database/history/hourly?startDate=${startDate}&endDate=${endDate}`,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
renderHourlyChart(data);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load hourly statistics');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadSlowQueries() {
|
||||
$.ajax({
|
||||
url: `/admin/database/history/slow-queries?startDate=${startDate}&endDate=${endDate}&limit=10`,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
renderSlowQueriesTable(data.queries);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load slow queries');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadQueryDetails(queryHash) {
|
||||
currentQueryHash = queryHash;
|
||||
|
||||
// Load query trend
|
||||
$.ajax({
|
||||
url: `/admin/database/history/query-trend/${queryHash}?startDate=${startDate}&endDate=${endDate}`,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
renderQueryTrendChart(data);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load query trend');
|
||||
}
|
||||
});
|
||||
|
||||
// Load query details
|
||||
$.ajax({
|
||||
url: `/admin/database/history/query-details/${queryHash}?limit=20`,
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
renderQueryDetailsTable(data.details);
|
||||
},
|
||||
error: function() {
|
||||
console.error('Failed to load query details');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderDailyChart(data) {
|
||||
const ctx = document.getElementById('daily-chart').getContext('2d');
|
||||
|
||||
if (dailyChart) {
|
||||
dailyChart.destroy();
|
||||
}
|
||||
|
||||
dailyChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
'y-queries': {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Query Count'
|
||||
}
|
||||
},
|
||||
'y-time': {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time (ms)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderHourlyChart(data) {
|
||||
const ctx = document.getElementById('hourly-chart').getContext('2d');
|
||||
|
||||
if (hourlyChart) {
|
||||
hourlyChart.destroy();
|
||||
}
|
||||
|
||||
hourlyChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
'y-queries': {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Query Count'
|
||||
}
|
||||
},
|
||||
'y-time': {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time (ms)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderQueryTrendChart(data) {
|
||||
const ctx = document.getElementById('query-trend-chart').getContext('2d');
|
||||
|
||||
if (queryTrendChart) {
|
||||
queryTrendChart.destroy();
|
||||
}
|
||||
|
||||
queryTrendChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
'y-count': {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Execution Count'
|
||||
}
|
||||
},
|
||||
'y-time': {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time (ms)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderSlowQueriesTable(queries) {
|
||||
if (!queries || queries.length === 0) {
|
||||
$('#slow-queries-table tbody').html('<tr><td colspan="6" class="text-center">No slow queries found</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
const html = queries.map(query => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(query.sql)}</code></td>
|
||||
<td>${query.execution_count}</td>
|
||||
<td>${parseFloat(query.avg_time).toFixed(2)}</td>
|
||||
<td>${parseFloat(query.max_time).toFixed(2)}</td>
|
||||
<td>${parseFloat(query.min_time).toFixed(2)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary view-details" data-query-hash="${query.query_hash}" data-sql="${escapeHtml(query.sql)}">
|
||||
View Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
$('#slow-queries-table tbody').html(html);
|
||||
|
||||
// Set up view details buttons
|
||||
$('.view-details').click(function() {
|
||||
const queryHash = $(this).data('query-hash');
|
||||
const sql = $(this).data('sql');
|
||||
|
||||
$('#query-details-sql').text(sql);
|
||||
$('#query-details-modal-label').text('Query Details');
|
||||
$('#query-details-modal').modal('show');
|
||||
|
||||
loadQueryDetails(queryHash);
|
||||
});
|
||||
}
|
||||
|
||||
function renderQueryDetailsTable(details) {
|
||||
if (!details || details.length === 0) {
|
||||
$('#query-details-table tbody').html('<tr><td colspan="4" class="text-center">No details found</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
const html = details.map(detail => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(detail.parameters)}</code></td>
|
||||
<td>${parseFloat(detail.execution_time).toFixed(2)}</td>
|
||||
<td>${detail.connection_name}</td>
|
||||
<td>${new Date(detail.timestamp * 1000).toLocaleString()}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
$('#query-details-table tbody').html(html);
|
||||
}
|
||||
|
||||
function getDateRange(range) {
|
||||
const today = new Date();
|
||||
let start = new Date();
|
||||
let end = new Date();
|
||||
|
||||
switch (range) {
|
||||
case 'today':
|
||||
// Start and end are already today
|
||||
break;
|
||||
|
||||
case 'yesterday':
|
||||
start.setDate(today.getDate() - 1);
|
||||
end.setDate(today.getDate() - 1);
|
||||
break;
|
||||
|
||||
case 'last7days':
|
||||
start.setDate(today.getDate() - 6);
|
||||
break;
|
||||
|
||||
case 'last30days':
|
||||
start.setDate(today.getDate() - 29);
|
||||
break;
|
||||
|
||||
case 'thismonth':
|
||||
start = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
end = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
break;
|
||||
|
||||
case 'lastmonth':
|
||||
start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||
end = new Date(today.getFullYear(), today.getMonth(), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
start: formatDate(start),
|
||||
end: formatDate(end)
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user