- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
497 lines
17 KiB
PHP
497 lines
17 KiB
PHP
@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
|