- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
658 lines
20 KiB
PHP
658 lines
20 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Error Handler
|
|
* Comprehensive error handling and logging system for sync operations
|
|
*
|
|
* @package DeskMoloni
|
|
* @subpackage Libraries
|
|
* @category ErrorHandling
|
|
* @author Descomplicar® - PHP Fullstack Engineer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
namespace DeskMoloni\Libraries;
|
|
|
|
class ErrorHandler
|
|
{
|
|
protected $CI;
|
|
protected $model;
|
|
|
|
// Error severity levels
|
|
const SEVERITY_LOW = 'low';
|
|
const SEVERITY_MEDIUM = 'medium';
|
|
const SEVERITY_HIGH = 'high';
|
|
const SEVERITY_CRITICAL = 'critical';
|
|
|
|
// Error categories
|
|
const CATEGORY_SYNC = 'sync';
|
|
const CATEGORY_API = 'api';
|
|
const CATEGORY_QUEUE = 'queue';
|
|
const CATEGORY_MAPPING = 'mapping';
|
|
const CATEGORY_VALIDATION = 'validation';
|
|
const CATEGORY_AUTHENTICATION = 'authentication';
|
|
const CATEGORY_SYSTEM = 'system';
|
|
|
|
// Error codes
|
|
const ERROR_API_CONNECTION = 'API_CONNECTION_FAILED';
|
|
const ERROR_API_TIMEOUT = 'API_TIMEOUT';
|
|
const ERROR_API_AUTHENTICATION = 'API_AUTHENTICATION_FAILED';
|
|
const ERROR_API_RATE_LIMIT = 'API_RATE_LIMIT_EXCEEDED';
|
|
const ERROR_API_INVALID_RESPONSE = 'API_INVALID_RESPONSE';
|
|
const ERROR_SYNC_CONFLICT = 'SYNC_CONFLICT';
|
|
const ERROR_SYNC_VALIDATION = 'SYNC_VALIDATION_FAILED';
|
|
const ERROR_MAPPING_NOT_FOUND = 'MAPPING_NOT_FOUND';
|
|
const ERROR_QUEUE_PROCESSING = 'QUEUE_PROCESSING_FAILED';
|
|
const ERROR_DATA_CORRUPTION = 'DATA_CORRUPTION';
|
|
const ERROR_SYSTEM_RESOURCE = 'SYSTEM_RESOURCE_EXHAUSTED';
|
|
|
|
// Notification settings
|
|
protected $notification_thresholds = [
|
|
self::SEVERITY_CRITICAL => 1,
|
|
self::SEVERITY_HIGH => 3,
|
|
self::SEVERITY_MEDIUM => 10,
|
|
self::SEVERITY_LOW => 50
|
|
];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->CI = &get_instance();
|
|
$this->CI->load->model('desk_moloni_model');
|
|
$this->model = $this->CI->desk_moloni_model;
|
|
|
|
log_activity('ErrorHandler initialized');
|
|
}
|
|
|
|
/**
|
|
* Log error with context and severity
|
|
*
|
|
* @param string $category
|
|
* @param string $error_code
|
|
* @param string $message
|
|
* @param array $context
|
|
* @param string $severity
|
|
* @return int Error log ID
|
|
*/
|
|
public function log_error($category, $error_code, $message, $context = [], $severity = self::SEVERITY_MEDIUM)
|
|
{
|
|
try {
|
|
// Validate inputs
|
|
if (!$this->is_valid_category($category)) {
|
|
$category = self::CATEGORY_SYSTEM;
|
|
}
|
|
|
|
if (!$this->is_valid_severity($severity)) {
|
|
$severity = self::SEVERITY_MEDIUM;
|
|
}
|
|
|
|
// Prepare error data
|
|
$error_data = [
|
|
'category' => $category,
|
|
'error_code' => $error_code,
|
|
'severity' => $severity,
|
|
'message' => $this->sanitize_message($message),
|
|
'context' => json_encode($this->sanitize_context($context)),
|
|
'stack_trace' => $this->get_sanitized_stack_trace(),
|
|
'occurred_at' => date('Y-m-d H:i:s'),
|
|
'user_id' => get_staff_user_id() ?: null,
|
|
'ip_address' => $this->CI->input->ip_address(),
|
|
'user_agent' => $this->CI->input->user_agent(),
|
|
'request_uri' => $this->CI->uri->uri_string(),
|
|
'memory_usage' => memory_get_usage(true),
|
|
'peak_memory' => memory_get_peak_usage(true),
|
|
'processing_time' => $this->get_processing_time()
|
|
];
|
|
|
|
// Store error in database
|
|
$error_id = $this->model->log_error($error_data);
|
|
|
|
// Log to file system as backup
|
|
$this->log_to_file($error_data);
|
|
|
|
// Check if notification is needed
|
|
$this->check_notification_threshold($category, $severity, $error_code);
|
|
|
|
// Trigger hooks for error handling
|
|
hooks()->do_action('desk_moloni_error_logged', $error_id, $error_data);
|
|
|
|
return $error_id;
|
|
|
|
} catch (\Exception $e) {
|
|
// Fallback error logging
|
|
log_message('error', 'ErrorHandler failed: ' . $e->getMessage());
|
|
error_log("DeskMoloni Error Handler Failure: {$e->getMessage()}");
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log API error with specific handling
|
|
*
|
|
* @param string $endpoint
|
|
* @param int $status_code
|
|
* @param string $response_body
|
|
* @param array $request_data
|
|
* @param string $error_message
|
|
* @return int
|
|
*/
|
|
public function log_api_error($endpoint, $status_code, $response_body, $request_data = [], $error_message = '')
|
|
{
|
|
$error_code = $this->determine_api_error_code($status_code, $response_body);
|
|
$severity = $this->determine_api_error_severity($status_code, $error_code);
|
|
|
|
$context = [
|
|
'endpoint' => $endpoint,
|
|
'status_code' => $status_code,
|
|
'response_body' => $this->truncate_response_body($response_body),
|
|
'request_data' => $this->sanitize_request_data($request_data),
|
|
'response_headers' => $this->get_last_response_headers()
|
|
];
|
|
|
|
$message = $error_message ?: "API request failed: {$endpoint} returned {$status_code}";
|
|
|
|
return $this->log_error(self::CATEGORY_API, $error_code, $message, $context, $severity);
|
|
}
|
|
|
|
/**
|
|
* Log sync error with entity context
|
|
*
|
|
* @param string $entity_type
|
|
* @param int $entity_id
|
|
* @param string $direction
|
|
* @param string $error_message
|
|
* @param array $additional_context
|
|
* @return int
|
|
*/
|
|
public function log_sync_error($entity_type, $entity_id, $direction, $error_message, $additional_context = [])
|
|
{
|
|
$error_code = $this->determine_sync_error_code($error_message);
|
|
$severity = $this->determine_sync_error_severity($error_code, $entity_type);
|
|
|
|
$context = array_merge([
|
|
'entity_type' => $entity_type,
|
|
'entity_id' => $entity_id,
|
|
'sync_direction' => $direction,
|
|
'sync_attempt' => $additional_context['attempt'] ?? 1
|
|
], $additional_context);
|
|
|
|
return $this->log_error(self::CATEGORY_SYNC, $error_code, $error_message, $context, $severity);
|
|
}
|
|
|
|
/**
|
|
* Log validation error
|
|
*
|
|
* @param string $field_name
|
|
* @param mixed $field_value
|
|
* @param string $validation_rule
|
|
* @param string $entity_type
|
|
* @return int
|
|
*/
|
|
public function log_validation_error($field_name, $field_value, $validation_rule, $entity_type = null)
|
|
{
|
|
$context = [
|
|
'field_name' => $field_name,
|
|
'field_value' => $this->sanitize_field_value($field_value),
|
|
'validation_rule' => $validation_rule,
|
|
'entity_type' => $entity_type
|
|
];
|
|
|
|
$message = "Validation failed for field '{$field_name}' with rule '{$validation_rule}'";
|
|
|
|
return $this->log_error(
|
|
self::CATEGORY_VALIDATION,
|
|
self::ERROR_SYNC_VALIDATION,
|
|
$message,
|
|
$context,
|
|
self::SEVERITY_LOW
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get error statistics
|
|
*
|
|
* @param array $filters
|
|
* @return array
|
|
*/
|
|
public function get_error_statistics($filters = [])
|
|
{
|
|
return [
|
|
'total_errors' => $this->model->count_errors($filters),
|
|
'by_category' => $this->model->count_errors_by_category($filters),
|
|
'by_severity' => $this->model->count_errors_by_severity($filters),
|
|
'by_error_code' => $this->model->count_errors_by_code($filters),
|
|
'recent_errors' => $this->model->get_recent_errors(10, $filters),
|
|
'error_trends' => $this->model->get_error_trends($filters),
|
|
'top_error_codes' => $this->model->get_top_error_codes(10, $filters)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get errors by criteria
|
|
*
|
|
* @param array $criteria
|
|
* @param int $limit
|
|
* @param int $offset
|
|
* @return array
|
|
*/
|
|
public function get_errors($criteria = [], $limit = 50, $offset = 0)
|
|
{
|
|
return $this->model->get_errors($criteria, $limit, $offset);
|
|
}
|
|
|
|
/**
|
|
* Mark error as resolved
|
|
*
|
|
* @param int $error_id
|
|
* @param string $resolution_notes
|
|
* @param int $resolved_by
|
|
* @return bool
|
|
*/
|
|
public function mark_error_resolved($error_id, $resolution_notes = '', $resolved_by = null)
|
|
{
|
|
$resolution_data = [
|
|
'resolved' => 1,
|
|
'resolved_at' => date('Y-m-d H:i:s'),
|
|
'resolved_by' => $resolved_by ?: get_staff_user_id(),
|
|
'resolution_notes' => $resolution_notes
|
|
];
|
|
|
|
$result = $this->model->update_error($error_id, $resolution_data);
|
|
|
|
if ($result) {
|
|
log_activity("Error #{$error_id} marked as resolved");
|
|
hooks()->do_action('desk_moloni_error_resolved', $error_id, $resolution_data);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Bulk mark errors as resolved
|
|
*
|
|
* @param array $error_ids
|
|
* @param string $resolution_notes
|
|
* @return array
|
|
*/
|
|
public function bulk_mark_resolved($error_ids, $resolution_notes = '')
|
|
{
|
|
$results = [
|
|
'total' => count($error_ids),
|
|
'success' => 0,
|
|
'errors' => 0
|
|
];
|
|
|
|
foreach ($error_ids as $error_id) {
|
|
if ($this->mark_error_resolved($error_id, $resolution_notes)) {
|
|
$results['success']++;
|
|
} else {
|
|
$results['errors']++;
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Clean up old errors
|
|
*
|
|
* @param int $retention_days
|
|
* @param bool $keep_critical
|
|
* @return int
|
|
*/
|
|
public function cleanup_old_errors($retention_days = 90, $keep_critical = true)
|
|
{
|
|
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$retention_days} days"));
|
|
|
|
$criteria = [
|
|
'occurred_before' => $cutoff_date,
|
|
'resolved' => 1
|
|
];
|
|
|
|
if ($keep_critical) {
|
|
$criteria['exclude_severity'] = self::SEVERITY_CRITICAL;
|
|
}
|
|
|
|
$deleted = $this->model->delete_errors($criteria);
|
|
|
|
if ($deleted > 0) {
|
|
log_activity("Cleaned up {$deleted} old error logs older than {$retention_days} days");
|
|
}
|
|
|
|
return $deleted;
|
|
}
|
|
|
|
/**
|
|
* Export errors to CSV
|
|
*
|
|
* @param array $filters
|
|
* @param int $limit
|
|
* @return string
|
|
*/
|
|
public function export_errors_csv($filters = [], $limit = 1000)
|
|
{
|
|
$errors = $this->model->get_errors($filters, $limit);
|
|
|
|
$output = fopen('php://temp', 'r+');
|
|
|
|
// CSV Header
|
|
fputcsv($output, [
|
|
'ID',
|
|
'Category',
|
|
'Error Code',
|
|
'Severity',
|
|
'Message',
|
|
'Occurred At',
|
|
'Resolved',
|
|
'User ID',
|
|
'IP Address',
|
|
'Request URI',
|
|
'Memory Usage',
|
|
'Context'
|
|
]);
|
|
|
|
foreach ($errors as $error) {
|
|
fputcsv($output, [
|
|
$error->id,
|
|
$error->category,
|
|
$error->error_code,
|
|
$error->severity,
|
|
$error->message,
|
|
$error->occurred_at,
|
|
$error->resolved ? 'Yes' : 'No',
|
|
$error->user_id,
|
|
$error->ip_address,
|
|
$error->request_uri,
|
|
$this->format_memory_usage($error->memory_usage),
|
|
$this->sanitize_context_for_export($error->context)
|
|
]);
|
|
}
|
|
|
|
rewind($output);
|
|
$csv_content = stream_get_contents($output);
|
|
fclose($output);
|
|
|
|
return $csv_content;
|
|
}
|
|
|
|
/**
|
|
* Check if notification threshold is reached
|
|
*
|
|
* @param string $category
|
|
* @param string $severity
|
|
* @param string $error_code
|
|
*/
|
|
protected function check_notification_threshold($category, $severity, $error_code)
|
|
{
|
|
$threshold = $this->notification_thresholds[$severity] ?? 10;
|
|
|
|
// Count recent errors of same type
|
|
$recent_count = $this->model->count_recent_errors($category, $error_code, 3600); // Last hour
|
|
|
|
if ($recent_count >= $threshold) {
|
|
$this->trigger_error_notification($category, $severity, $error_code, $recent_count);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trigger error notification
|
|
*
|
|
* @param string $category
|
|
* @param string $severity
|
|
* @param string $error_code
|
|
* @param int $error_count
|
|
*/
|
|
protected function trigger_error_notification($category, $severity, $error_code, $error_count)
|
|
{
|
|
$notification_data = [
|
|
'category' => $category,
|
|
'severity' => $severity,
|
|
'error_code' => $error_code,
|
|
'error_count' => $error_count,
|
|
'time_period' => '1 hour'
|
|
];
|
|
|
|
// Send email notification if configured
|
|
if (get_option('desk_moloni_error_notifications') == '1') {
|
|
$this->send_error_notification_email($notification_data);
|
|
}
|
|
|
|
// Trigger webhook if configured
|
|
if (get_option('desk_moloni_error_webhooks') == '1') {
|
|
$this->trigger_error_webhook($notification_data);
|
|
}
|
|
|
|
hooks()->do_action('desk_moloni_error_threshold_reached', $notification_data);
|
|
}
|
|
|
|
/**
|
|
* Send error notification email
|
|
*
|
|
* @param array $notification_data
|
|
*/
|
|
protected function send_error_notification_email($notification_data)
|
|
{
|
|
$admin_emails = explode(',', get_option('desk_moloni_admin_emails', ''));
|
|
|
|
if (empty($admin_emails)) {
|
|
return;
|
|
}
|
|
|
|
$subject = "Desk-Moloni Error Threshold Reached: {$notification_data['error_code']}";
|
|
$message = $this->build_error_notification_message($notification_data);
|
|
|
|
foreach ($admin_emails as $email) {
|
|
$email = trim($email);
|
|
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
send_mail_template('desk_moloni_error_notification', $email, [
|
|
'subject' => $subject,
|
|
'message' => $message,
|
|
'notification_data' => $notification_data
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine API error code from response
|
|
*
|
|
* @param int $status_code
|
|
* @param string $response_body
|
|
* @return string
|
|
*/
|
|
protected function determine_api_error_code($status_code, $response_body)
|
|
{
|
|
switch ($status_code) {
|
|
case 401:
|
|
case 403:
|
|
return self::ERROR_API_AUTHENTICATION;
|
|
case 429:
|
|
return self::ERROR_API_RATE_LIMIT;
|
|
case 408:
|
|
case 504:
|
|
return self::ERROR_API_TIMEOUT;
|
|
case 0:
|
|
return self::ERROR_API_CONNECTION;
|
|
default:
|
|
if ($status_code >= 500) {
|
|
return self::ERROR_API_CONNECTION;
|
|
} elseif ($status_code >= 400) {
|
|
return self::ERROR_API_INVALID_RESPONSE;
|
|
}
|
|
return 'API_UNKNOWN_ERROR';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine API error severity
|
|
*
|
|
* @param int $status_code
|
|
* @param string $error_code
|
|
* @return string
|
|
*/
|
|
protected function determine_api_error_severity($status_code, $error_code)
|
|
{
|
|
if (in_array($error_code, [self::ERROR_API_AUTHENTICATION, self::ERROR_API_CONNECTION])) {
|
|
return self::SEVERITY_CRITICAL;
|
|
}
|
|
|
|
if ($error_code === self::ERROR_API_RATE_LIMIT) {
|
|
return self::SEVERITY_HIGH;
|
|
}
|
|
|
|
if ($status_code >= 500) {
|
|
return self::SEVERITY_HIGH;
|
|
}
|
|
|
|
return self::SEVERITY_MEDIUM;
|
|
}
|
|
|
|
/**
|
|
* Sanitize error message
|
|
*
|
|
* @param string $message
|
|
* @return string
|
|
*/
|
|
protected function sanitize_message($message)
|
|
{
|
|
// Remove sensitive information patterns
|
|
$patterns = [
|
|
'/password[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i',
|
|
'/token[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i',
|
|
'/key[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i',
|
|
'/secret[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i'
|
|
];
|
|
|
|
$message = preg_replace($patterns, '[REDACTED]', $message);
|
|
|
|
return substr(trim($message), 0, 1000);
|
|
}
|
|
|
|
/**
|
|
* Sanitize context data
|
|
*
|
|
* @param array $context
|
|
* @return array
|
|
*/
|
|
protected function sanitize_context($context)
|
|
{
|
|
$sensitive_keys = ['password', 'token', 'key', 'secret', 'auth', 'credential'];
|
|
|
|
array_walk_recursive($context, function(&$value, $key) use ($sensitive_keys) {
|
|
if (is_string($key) && in_array(strtolower($key), $sensitive_keys)) {
|
|
$value = '[REDACTED]';
|
|
}
|
|
});
|
|
|
|
return $context;
|
|
}
|
|
|
|
/**
|
|
* Get sanitized stack trace
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_sanitized_stack_trace()
|
|
{
|
|
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
|
|
|
|
$clean_trace = [];
|
|
foreach ($trace as $frame) {
|
|
$clean_frame = [
|
|
'file' => basename($frame['file'] ?? 'unknown'),
|
|
'line' => $frame['line'] ?? 0,
|
|
'function' => $frame['function'] ?? 'unknown'
|
|
];
|
|
|
|
if (isset($frame['class'])) {
|
|
$clean_frame['class'] = $frame['class'];
|
|
}
|
|
|
|
$clean_trace[] = $clean_frame;
|
|
}
|
|
|
|
return json_encode($clean_trace);
|
|
}
|
|
|
|
/**
|
|
* Validate error category
|
|
*
|
|
* @param string $category
|
|
* @return bool
|
|
*/
|
|
protected function is_valid_category($category)
|
|
{
|
|
return in_array($category, [
|
|
self::CATEGORY_SYNC,
|
|
self::CATEGORY_API,
|
|
self::CATEGORY_QUEUE,
|
|
self::CATEGORY_MAPPING,
|
|
self::CATEGORY_VALIDATION,
|
|
self::CATEGORY_AUTHENTICATION,
|
|
self::CATEGORY_SYSTEM
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Validate error severity
|
|
*
|
|
* @param string $severity
|
|
* @return bool
|
|
*/
|
|
protected function is_valid_severity($severity)
|
|
{
|
|
return in_array($severity, [
|
|
self::SEVERITY_LOW,
|
|
self::SEVERITY_MEDIUM,
|
|
self::SEVERITY_HIGH,
|
|
self::SEVERITY_CRITICAL
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Log error to file as backup
|
|
*
|
|
* @param array $error_data
|
|
*/
|
|
protected function log_to_file($error_data)
|
|
{
|
|
$log_file = FCPATH . 'uploads/desk_moloni/logs/errors_' . date('Y-m-d') . '.log';
|
|
|
|
$log_entry = sprintf(
|
|
"[%s] %s/%s: %s\n",
|
|
$error_data['occurred_at'],
|
|
$error_data['category'],
|
|
$error_data['severity'],
|
|
$error_data['message']
|
|
);
|
|
|
|
if (!empty($error_data['context'])) {
|
|
$log_entry .= "Context: " . $error_data['context'] . "\n";
|
|
}
|
|
|
|
$log_entry .= "---\n";
|
|
|
|
file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
|
|
}
|
|
|
|
/**
|
|
* Get current processing time
|
|
*
|
|
* @return float
|
|
*/
|
|
protected function get_processing_time()
|
|
{
|
|
if (defined('APP_START_TIME')) {
|
|
return microtime(true) - APP_START_TIME;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
} |