Improve error log deduplication and occurrence tracking
Enhanced error deduplication by matching on type, file, line, and message. Updated error occurrence counting and admin stats to reflect deduplicated errors. Refactored error resolution and deletion to operate on all matching errors. Improved error occurrence display in the admin detail view for clarity and accuracy.
This commit is contained in:
@@ -229,9 +229,20 @@ class DebugController extends Controller
|
|||||||
if (isset($entity['vcardArray'][1])) {
|
if (isset($entity['vcardArray'][1])) {
|
||||||
foreach ($entity['vcardArray'][1] as $field) {
|
foreach ($entity['vcardArray'][1] as $field) {
|
||||||
if (is_array($field) && count($field) >= 4) {
|
if (is_array($field) && count($field) >= 4) {
|
||||||
|
// Handle nested arrays by flattening them recursively
|
||||||
|
$value = $field[3];
|
||||||
|
if (is_array($value)) {
|
||||||
|
$flattened = [];
|
||||||
|
array_walk_recursive($value, function($item) use (&$flattened) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
$flattened[] = $item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$value = !empty($flattened) ? implode(', ', $flattened) : json_encode($value);
|
||||||
|
}
|
||||||
$parsedData[] = [
|
$parsedData[] = [
|
||||||
'key' => $field[0],
|
'key' => $field[0],
|
||||||
'value' => is_array($field[3]) ? implode(', ', $field[3]) : $field[3]
|
'value' => $value
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,51 +15,52 @@ class ErrorLog extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an error to database
|
* Log an error to database
|
||||||
* If the same error exists (same file + line + type), increment occurrence count
|
* If the same error exists (same type + file + line + message), increment occurrence count
|
||||||
|
* Otherwise, create a new record with unique error_id
|
||||||
*/
|
*/
|
||||||
public function logError(array $errorData): ?int
|
public function logError(array $errorData): ?int
|
||||||
{
|
{
|
||||||
// Generate unique error signature for deduplication
|
// Check if this error already exists (same type, file, line, and message)
|
||||||
$signature = md5($errorData['error_type'] . $errorData['error_file'] . $errorData['error_line']);
|
|
||||||
|
|
||||||
// Check if this error already exists
|
|
||||||
$existing = $this->findBySimilar(
|
$existing = $this->findBySimilar(
|
||||||
$errorData['error_type'],
|
$errorData['error_type'],
|
||||||
$errorData['error_file'],
|
$errorData['error_file'],
|
||||||
$errorData['error_line']
|
$errorData['error_line'],
|
||||||
|
$errorData['error_message']
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
// Update existing error
|
// Update existing error: increment occurrence and update timestamp
|
||||||
|
// Keep the original error_id (don't use the new one from errorData)
|
||||||
$this->incrementOccurrence($existing['id']);
|
$this->incrementOccurrence($existing['id']);
|
||||||
return $existing['id'];
|
return $existing['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new error log
|
// Create new error log with the unique error_id
|
||||||
return $this->create($errorData);
|
return $this->create($errorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find similar error (same type, file, line)
|
* Find similar error (same type, file, line, and message)
|
||||||
*/
|
*/
|
||||||
private function findBySimilar(string $type, string $file, int $line): ?array
|
private function findBySimilar(string $type, string $file, int $line, string $message): ?array
|
||||||
{
|
{
|
||||||
$sql = "SELECT * FROM error_logs
|
$sql = "SELECT * FROM error_logs
|
||||||
WHERE error_type = ?
|
WHERE error_type = ?
|
||||||
AND error_file = ?
|
AND error_file = ?
|
||||||
AND error_line = ?
|
AND error_line = ?
|
||||||
|
AND error_message = ?
|
||||||
AND is_resolved = FALSE
|
AND is_resolved = FALSE
|
||||||
LIMIT 1";
|
LIMIT 1";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([$type, $file, $line]);
|
$stmt->execute([$type, $file, $line, $message]);
|
||||||
$result = $stmt->fetch();
|
$result = $stmt->fetch();
|
||||||
|
|
||||||
return $result ?: null;
|
return $result ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment occurrence counter
|
* Increment occurrence counter and update last occurrence timestamp
|
||||||
*/
|
*/
|
||||||
private function incrementOccurrence(int $id): void
|
private function incrementOccurrence(int $id): void
|
||||||
{
|
{
|
||||||
@@ -72,6 +73,7 @@ class ErrorLog extends Model
|
|||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find error by error_id (unique reference)
|
* Find error by error_id (unique reference)
|
||||||
*/
|
*/
|
||||||
@@ -256,12 +258,11 @@ class ErrorLog extends Model
|
|||||||
error_file,
|
error_file,
|
||||||
error_line,
|
error_line,
|
||||||
is_resolved,
|
is_resolved,
|
||||||
MIN(occurred_at) as occurred_at,
|
occurred_at,
|
||||||
MAX(occurred_at) as last_occurred_at,
|
last_occurred_at,
|
||||||
COUNT(*) as occurrences
|
occurrences
|
||||||
FROM error_logs
|
FROM error_logs
|
||||||
$whereClause
|
$whereClause
|
||||||
GROUP BY error_id
|
|
||||||
ORDER BY $sortColumn $sortOrder
|
ORDER BY $sortColumn $sortOrder
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
";
|
";
|
||||||
@@ -291,7 +292,7 @@ class ErrorLog extends Model
|
|||||||
|
|
||||||
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
$query = "SELECT COUNT(DISTINCT error_id) as total FROM error_logs $whereClause";
|
$query = "SELECT COUNT(*) as total FROM error_logs $whereClause";
|
||||||
$stmt = $this->db->prepare($query);
|
$stmt = $this->db->prepare($query);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
return (int)$stmt->fetch()['total'];
|
return (int)$stmt->fetch()['total'];
|
||||||
@@ -299,16 +300,19 @@ class ErrorLog extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all occurrences of a specific error
|
* Get all occurrences of a specific error
|
||||||
|
* Since we deduplicate errors, this returns a single record (or empty if not found)
|
||||||
|
* The occurrences count shows how many times this error happened
|
||||||
*/
|
*/
|
||||||
public function getOccurrencesByErrorId(string $errorId): array
|
public function getOccurrencesByErrorId(string $errorId): array
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare("
|
||||||
SELECT * FROM error_logs
|
SELECT * FROM error_logs
|
||||||
WHERE error_id = ?
|
WHERE error_id = ?
|
||||||
ORDER BY occurred_at DESC
|
LIMIT 1
|
||||||
");
|
");
|
||||||
$stmt->execute([$errorId]);
|
$stmt->execute([$errorId]);
|
||||||
return $stmt->fetchAll();
|
$result = $stmt->fetch();
|
||||||
|
return $result ? [$result] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,16 +320,16 @@ class ErrorLog extends Model
|
|||||||
*/
|
*/
|
||||||
public function getAdminStats(): array
|
public function getAdminStats(): array
|
||||||
{
|
{
|
||||||
// Total unique errors
|
// Total unique errors (one record per unique error signature)
|
||||||
$stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs");
|
||||||
$totalErrors = $stmt->fetch()['total'];
|
$totalErrors = $stmt->fetch()['total'];
|
||||||
|
|
||||||
// Unresolved errors
|
// Unresolved errors
|
||||||
$stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs WHERE is_resolved = 0");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs WHERE is_resolved = 0");
|
||||||
$unresolved = $stmt->fetch()['total'];
|
$unresolved = $stmt->fetch()['total'];
|
||||||
|
|
||||||
// Errors in last 24h
|
// Errors in last 24h (errors that occurred or were last seen in last 24h)
|
||||||
$stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs WHERE occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs WHERE last_occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)");
|
||||||
$last24h = $stmt->fetch()['total'];
|
$last24h = $stmt->fetch()['total'];
|
||||||
|
|
||||||
// Total occurrences
|
// Total occurrences
|
||||||
@@ -342,45 +346,99 @@ class ErrorLog extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark all occurrences of an error as resolved
|
* Mark all occurrences of an error as resolved
|
||||||
|
* Resolves all errors with the same type, file, line, and message as the given error_id
|
||||||
*/
|
*/
|
||||||
public function markErrorResolved(string $errorId, int $userId, ?string $notes): bool
|
public function markErrorResolved(string $errorId, int $userId, ?string $notes): bool
|
||||||
{
|
{
|
||||||
|
// First get the error details to find all similar errors
|
||||||
|
$error = $this->findByErrorId($errorId);
|
||||||
|
if (!$error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all errors with the same signature as resolved
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare("
|
||||||
UPDATE error_logs
|
UPDATE error_logs
|
||||||
SET is_resolved = 1,
|
SET is_resolved = 1,
|
||||||
resolved_at = NOW(),
|
resolved_at = NOW(),
|
||||||
resolved_by = ?,
|
resolved_by = ?,
|
||||||
notes = ?
|
notes = ?
|
||||||
WHERE error_id = ?
|
WHERE error_type = ?
|
||||||
|
AND error_file = ?
|
||||||
|
AND error_line = ?
|
||||||
|
AND error_message = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
return $stmt->execute([$userId, $notes, $errorId]);
|
return $stmt->execute([
|
||||||
|
$userId,
|
||||||
|
$notes,
|
||||||
|
$error['error_type'],
|
||||||
|
$error['error_file'],
|
||||||
|
$error['error_line'],
|
||||||
|
$error['error_message']
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark all occurrences of an error as unresolved
|
* Mark all occurrences of an error as unresolved
|
||||||
|
* Unresolves all errors with the same type, file, line, and message as the given error_id
|
||||||
*/
|
*/
|
||||||
public function markErrorUnresolved(string $errorId): bool
|
public function markErrorUnresolved(string $errorId): bool
|
||||||
{
|
{
|
||||||
|
// First get the error details to find all similar errors
|
||||||
|
$error = $this->findByErrorId($errorId);
|
||||||
|
if (!$error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all errors with the same signature as unresolved
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare("
|
||||||
UPDATE error_logs
|
UPDATE error_logs
|
||||||
SET is_resolved = 0,
|
SET is_resolved = 0,
|
||||||
resolved_at = NULL,
|
resolved_at = NULL,
|
||||||
resolved_by = NULL,
|
resolved_by = NULL,
|
||||||
notes = NULL
|
notes = NULL
|
||||||
WHERE error_id = ?
|
WHERE error_type = ?
|
||||||
|
AND error_file = ?
|
||||||
|
AND error_line = ?
|
||||||
|
AND error_message = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
return $stmt->execute([$errorId]);
|
return $stmt->execute([
|
||||||
|
$error['error_type'],
|
||||||
|
$error['error_file'],
|
||||||
|
$error['error_line'],
|
||||||
|
$error['error_message']
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all occurrences of an error
|
* Delete all occurrences of an error
|
||||||
|
* Deletes all errors with the same type, file, line, and message as the given error_id
|
||||||
*/
|
*/
|
||||||
public function deleteByErrorId(string $errorId): bool
|
public function deleteByErrorId(string $errorId): bool
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("DELETE FROM error_logs WHERE error_id = ?");
|
// First get the error details to find all similar errors
|
||||||
return $stmt->execute([$errorId]);
|
$error = $this->findByErrorId($errorId);
|
||||||
|
if (!$error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all errors with the same signature
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
DELETE FROM error_logs
|
||||||
|
WHERE error_type = ?
|
||||||
|
AND error_file = ?
|
||||||
|
AND error_line = ?
|
||||||
|
AND error_message = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
return $stmt->execute([
|
||||||
|
$error['error_type'],
|
||||||
|
$error['error_file'],
|
||||||
|
$error['error_line'],
|
||||||
|
$error['error_message']
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="fas fa-redo mr-1.5"></i>
|
<i class="fas fa-redo mr-1.5"></i>
|
||||||
<span><?= count($errorOccurrences) ?> occurrence<?= count($errorOccurrences) != 1 ? 's' : '' ?></span>
|
<span><?= $error['occurrences'] ?? 1 ?> occurrence<?= ($error['occurrences'] ?? 1) != 1 ? 's' : '' ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="far fa-clock mr-1.5"></i>
|
<i class="far fa-clock mr-1.5"></i>
|
||||||
<span>Last: <?= date('M d, Y H:i:s', strtotime($error['occurred_at'])) ?></span>
|
<span>Last: <?= date('M d, Y H:i:s', strtotime($error['last_occurred_at'] ?? $error['occurred_at'])) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +134,7 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro
|
|||||||
</button>
|
</button>
|
||||||
<button onclick="switchTab('occurrences')" id="tab-occurrences" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
<button onclick="switchTab('occurrences')" id="tab-occurrences" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||||
<i class="fas fa-history mr-2"></i>
|
<i class="fas fa-history mr-2"></i>
|
||||||
All Occurrences (<?= count($errorOccurrences) ?>)
|
Occurrence Details (<?= $error['occurrences'] ?? 1 ?>)
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,24 +212,47 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro
|
|||||||
|
|
||||||
<!-- Occurrences Tab -->
|
<!-- Occurrences Tab -->
|
||||||
<div id="content-occurrences" class="tab-content hidden">
|
<div id="content-occurrences" class="tab-content hidden">
|
||||||
<div class="space-y-2">
|
<div class="space-y-4">
|
||||||
<?php foreach ($errorOccurrences as $occurrence): ?>
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
<div class="flex items-start">
|
||||||
<div class="flex items-center justify-between">
|
<i class="fas fa-info-circle text-blue-600 mt-0.5 mr-3"></i>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-900"><?= date('M d, Y H:i:s', strtotime($occurrence['occurred_at'])) ?></p>
|
<p class="text-sm font-semibold text-blue-900 mb-1">Error Occurrence Tracking</p>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<p class="text-sm text-blue-800">
|
||||||
<?= htmlspecialchars($occurrence['request_method']) ?>
|
This error has occurred <strong><?= $error['occurrences'] ?? 1 ?> time<?= ($error['occurrences'] ?? 1) != 1 ? 's' : '' ?></strong>.
|
||||||
<?= htmlspecialchars($occurrence['request_uri']) ?>
|
Similar errors are automatically grouped together and the occurrence count is incremented.
|
||||||
from <?= htmlspecialchars($occurrence['ip_address']) ?>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500">
|
</div>
|
||||||
ID: <span class="font-mono"><?= $occurrence['id'] ?></span>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Occurrence Information</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">First Occurred</p>
|
||||||
|
<p class="text-sm text-gray-900"><?= date('M d, Y H:i:s', strtotime($error['occurred_at'])) ?></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Last Occurred</p>
|
||||||
|
<p class="text-sm text-gray-900"><?= date('M d, Y H:i:s', strtotime($error['last_occurred_at'] ?? $error['occurred_at'])) ?></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Total Occurrences</p>
|
||||||
|
<p class="text-sm text-gray-900"><?= $error['occurrences'] ?? 1 ?></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Request Details</p>
|
||||||
|
<p class="text-xs text-gray-600">
|
||||||
|
<?= htmlspecialchars($error['request_method']) ?>
|
||||||
|
<?= htmlspecialchars($error['request_uri']) ?>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-600 mt-1">
|
||||||
|
IP: <?= htmlspecialchars($error['ip_address']) ?>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +272,7 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">First Occurred</p>
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">First Occurred</p>
|
||||||
<p class="text-sm text-gray-900"><?= date('M d, Y H:i:s', strtotime($errorOccurrences[count($errorOccurrences)-1]['occurred_at'])) ?></p>
|
<p class="text-sm text-gray-900"><?= date('M d, Y H:i:s', strtotime($error['occurred_at'])) ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user