Files
domnitor/app/Models/ErrorLog.php
Hosteroid dcb7f685dd Enhance error resolution workflow and notification service
Refactored error log model and views to use a unified 'notes' field instead of 'resolution_notes'. Added a modal dialog for entering resolution notes when marking errors as resolved in admin views. Improved stack trace handling in ErrorHandler by storing as JSON and formatting for display. Expanded NotificationService to support multi-channel notifications (email, Telegram, Discord, Slack), group notifications, and improved domain expiration alerts.
2025-10-11 20:27:46 +03:00

401 lines
12 KiB
PHP

<?php
namespace App\Models;
use Core\Model;
/**
* ErrorLog Model
*
* Manages error log database operations for tracking and debugging
*/
class ErrorLog extends Model
{
protected static string $table = 'error_logs';
/**
* Log an error to database
* If the same error exists (same file + line + type), increment occurrence count
*/
public function logError(array $errorData): ?int
{
// Generate unique error signature for deduplication
$signature = md5($errorData['error_type'] . $errorData['error_file'] . $errorData['error_line']);
// Check if this error already exists
$existing = $this->findBySimilar(
$errorData['error_type'],
$errorData['error_file'],
$errorData['error_line']
);
if ($existing) {
// Update existing error
$this->incrementOccurrence($existing['id']);
return $existing['id'];
}
// Create new error log
return $this->create($errorData);
}
/**
* Find similar error (same type, file, line)
*/
private function findBySimilar(string $type, string $file, int $line): ?array
{
$sql = "SELECT * FROM error_logs
WHERE error_type = ?
AND error_file = ?
AND error_line = ?
AND is_resolved = FALSE
LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([$type, $file, $line]);
$result = $stmt->fetch();
return $result ?: null;
}
/**
* Increment occurrence counter
*/
private function incrementOccurrence(int $id): void
{
$sql = "UPDATE error_logs
SET occurrences = occurrences + 1,
last_occurred_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
}
/**
* Find error by error_id (unique reference)
*/
public function findByErrorId(string $errorId): ?array
{
$sql = "SELECT el.*, u.username, u.full_name, r.username as resolved_by_name
FROM error_logs el
LEFT JOIN users u ON el.user_id = u.id
LEFT JOIN users r ON el.resolved_by = r.id
WHERE el.error_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$errorId]);
$result = $stmt->fetch();
return $result ?: null;
}
/**
* Get recent errors with pagination
*/
public function getRecent(int $limit = 50, int $offset = 0, array $filters = []): array
{
$where = ['1=1'];
$params = [];
// Filter by resolution status
if (isset($filters['resolved'])) {
$where[] = 'el.is_resolved = ?';
$params[] = $filters['resolved'] ? 1 : 0;
}
// Filter by error type
if (!empty($filters['type'])) {
$where[] = 'el.error_type LIKE ?';
$params[] = '%' . $filters['type'] . '%';
}
// Filter by user
if (!empty($filters['user_id'])) {
$where[] = 'el.user_id = ?';
$params[] = $filters['user_id'];
}
$whereClause = implode(' AND ', $where);
$sql = "SELECT el.*, u.username, u.full_name
FROM error_logs el
LEFT JOIN users u ON el.user_id = u.id
WHERE {$whereClause}
ORDER BY el.last_occurred_at DESC
LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* Count total errors
*/
public function count(array $filters = []): int
{
$where = ['1=1'];
$params = [];
if (isset($filters['resolved'])) {
$where[] = 'is_resolved = ?';
$params[] = $filters['resolved'] ? 1 : 0;
}
if (!empty($filters['type'])) {
$where[] = 'error_type LIKE ?';
$params[] = '%' . $filters['type'] . '%';
}
if (!empty($filters['user_id'])) {
$where[] = 'user_id = ?';
$params[] = $filters['user_id'];
}
$whereClause = implode(' AND ', $where);
$sql = "SELECT COUNT(*) as count FROM error_logs WHERE {$whereClause}";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return (int)$stmt->fetch()['count'];
}
/**
* Get error statistics
*/
public function getStats(): array
{
$sql = "SELECT
COUNT(*) as total_errors,
SUM(occurrences) as total_occurrences,
COUNT(CASE WHEN is_resolved = FALSE THEN 1 END) as unresolved,
COUNT(CASE WHEN is_resolved = TRUE THEN 1 END) as resolved,
COUNT(CASE WHEN occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 1 END) as last_24h,
COUNT(CASE WHEN occurred_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as last_7d
FROM error_logs";
$stmt = $this->db->query($sql);
return $stmt->fetch();
}
/**
* Mark error as resolved
*/
public function resolve(int $id, int $resolvedBy, ?string $notes = null): bool
{
return $this->update($id, [
'is_resolved' => true,
'resolved_at' => date('Y-m-d H:i:s'),
'resolved_by' => $resolvedBy,
'notes' => $notes
]);
}
/**
* Delete old resolved errors
*/
public function deleteOldResolved(int $daysOld = 30): int
{
$sql = "DELETE FROM error_logs
WHERE is_resolved = TRUE
AND resolved_at < DATE_SUB(NOW(), INTERVAL ? DAY)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$daysOld]);
return $stmt->rowCount();
}
/**
* Get most frequent errors
*/
public function getMostFrequent(int $limit = 10): array
{
$sql = "SELECT el.*, u.username, u.full_name
FROM error_logs el
LEFT JOIN users u ON el.user_id = u.id
WHERE el.is_resolved = FALSE
ORDER BY el.occurrences DESC, el.last_occurred_at DESC
LIMIT ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
/**
* Get paginated errors with filters for admin panel
*/
public function getPaginatedErrors(array $filters, int $perPage, int $offset): array
{
$where = [];
$params = [];
if ($filters['resolved'] !== '') {
$where[] = 'is_resolved = ?';
$params[] = (int)$filters['resolved'];
}
if (!empty($filters['type'])) {
$where[] = 'error_type LIKE ?';
$params[] = '%' . $filters['type'] . '%';
}
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$sortColumn = $filters['sort'];
$sortOrder = strtoupper($filters['order']) === 'DESC' ? 'DESC' : 'ASC';
$query = "
SELECT
error_id,
error_type,
error_message,
error_file,
error_line,
is_resolved,
MIN(occurred_at) as occurred_at,
MAX(occurred_at) as last_occurred_at,
COUNT(*) as occurrences
FROM error_logs
$whereClause
GROUP BY error_id
ORDER BY $sortColumn $sortOrder
LIMIT ? OFFSET ?
";
$stmt = $this->db->prepare($query);
$stmt->execute([...$params, $perPage, $offset]);
return $stmt->fetchAll();
}
/**
* Count total unique errors with filters
*/
public function countUniqueErrors(array $filters): int
{
$where = [];
$params = [];
if ($filters['resolved'] !== '') {
$where[] = 'is_resolved = ?';
$params[] = (int)$filters['resolved'];
}
if (!empty($filters['type'])) {
$where[] = 'error_type LIKE ?';
$params[] = '%' . $filters['type'] . '%';
}
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$query = "SELECT COUNT(DISTINCT error_id) as total FROM error_logs $whereClause";
$stmt = $this->db->prepare($query);
$stmt->execute($params);
return (int)$stmt->fetch()['total'];
}
/**
* Get all occurrences of a specific error
*/
public function getOccurrencesByErrorId(string $errorId): array
{
$stmt = $this->db->prepare("
SELECT * FROM error_logs
WHERE error_id = ?
ORDER BY occurred_at DESC
");
$stmt->execute([$errorId]);
return $stmt->fetchAll();
}
/**
* Get admin statistics
*/
public function getAdminStats(): array
{
// Total unique errors
$stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs");
$totalErrors = $stmt->fetch()['total'];
// Unresolved errors
$stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs WHERE is_resolved = 0");
$unresolved = $stmt->fetch()['total'];
// Errors 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)");
$last24h = $stmt->fetch()['total'];
// Total occurrences
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs");
$totalOccurrences = $stmt->fetch()['total'];
return [
'total_errors' => $totalErrors,
'unresolved' => $unresolved,
'last_24h' => $last24h,
'total_occurrences' => $totalOccurrences
];
}
/**
* Mark all occurrences of an error as resolved
*/
public function markErrorResolved(string $errorId, int $userId, ?string $notes): bool
{
$stmt = $this->db->prepare("
UPDATE error_logs
SET is_resolved = 1,
resolved_at = NOW(),
resolved_by = ?,
notes = ?
WHERE error_id = ?
");
return $stmt->execute([$userId, $notes, $errorId]);
}
/**
* Mark all occurrences of an error as unresolved
*/
public function markErrorUnresolved(string $errorId): bool
{
$stmt = $this->db->prepare("
UPDATE error_logs
SET is_resolved = 0,
resolved_at = NULL,
resolved_by = NULL,
notes = NULL
WHERE error_id = ?
");
return $stmt->execute([$errorId]);
}
/**
* Delete all occurrences of an error
*/
public function deleteByErrorId(string $errorId): bool
{
$stmt = $this->db->prepare("DELETE FROM error_logs WHERE error_id = ?");
return $stmt->execute([$errorId]);
}
/**
* Clear old resolved errors
*/
public function clearOldResolved(int $daysOld): int
{
$stmt = $this->db->prepare("
DELETE FROM error_logs
WHERE is_resolved = 1
AND resolved_at < DATE_SUB(NOW(), INTERVAL ? DAY)
");
$stmt->execute([$daysOld]);
return $stmt->rowCount();
}
}