PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.5.23
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.5.23
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-error-logger.php

class-metasync-error-logger.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.5.23, at includes/class-metasync-error-logger.php

357 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Enhanced Error Logger with Categories and Hooks
4 *
5 * Implements structured error categorization with business logic categories
6 * for better integration with external monitoring tools.
7 *
8 * @package Metasync
9 * @subpackage Metasync/includes
10 * @since 1.0.0
11 * @author Engineering Team <support@searchatlas.com>
12 */
13
14 // If this file is called directly, abort.
15 if (!defined('WPINC')) {
16 die;
17 }
18
19 /**
20 * Enhanced Error Logger Class
21 *
22 * Provides structured error logging with:
23 * - 8 business logic error categories
24 * - Standardized error codes (MS-XXXX format)
25 * - Structured log format with JSON context
26 * - WordPress action hooks for external monitoring
27 * - Error summary tracking in wp_options
28 */
29 class Metasync_Error_Logger {
30
31 /**
32 * Error category constants
33 */
34 const CATEGORY_API_RATE_LIMIT = 'API_RATE_LIMIT';
35 const CATEGORY_API_BACKOFF = 'API_BACKOFF';
36 const CATEGORY_EXECUTION_TIMEOUT = 'EXECUTION_TIMEOUT';
37 const CATEGORY_MEMORY_EXHAUSTED = 'MEMORY_EXHAUSTED';
38 const CATEGORY_QUEUE_OVERFLOW = 'QUEUE_OVERFLOW';
39 const CATEGORY_AUTHENTICATION_FAILURE = 'AUTHENTICATION_FAILURE';
40 const CATEGORY_DATABASE_ERROR = 'DATABASE_ERROR';
41 const CATEGORY_NETWORK_ERROR = 'NETWORK_ERROR';
42
43 /**
44 * Error codes mapping
45 * Format: MS-XXXX
46 */
47 private static $error_codes = [
48 self::CATEGORY_API_RATE_LIMIT => 'MS-1001',
49 self::CATEGORY_API_BACKOFF => 'MS-1002',
50 self::CATEGORY_EXECUTION_TIMEOUT => 'MS-2001',
51 self::CATEGORY_MEMORY_EXHAUSTED => 'MS-2002',
52 self::CATEGORY_QUEUE_OVERFLOW => 'MS-3001',
53 self::CATEGORY_AUTHENTICATION_FAILURE => 'MS-4001',
54 self::CATEGORY_DATABASE_ERROR => 'MS-5001',
55 self::CATEGORY_NETWORK_ERROR => 'MS-6001',
56 ];
57
58 /**
59 * Severity level constants
60 */
61 const SEVERITY_INFO = 'INFO';
62 const SEVERITY_WARNING = 'WARNING';
63 const SEVERITY_ERROR = 'ERROR';
64 const SEVERITY_CRITICAL = 'CRITICAL';
65
66 /**
67 * Option name for error summary storage
68 */
69 const ERROR_SUMMARY_OPTION = 'metasync_error_summary';
70
71 /**
72 * Maximum number of unique errors to keep in summary
73 */
74 const MAX_SUMMARY_ENTRIES = 100;
75
76 /**
77 * Log file name
78 */
79 const LOG_FILE_NAME = 'metasync-errors.log';
80
81 /**
82 * Main logging function
83 *
84 * Formats and writes structured error log with:
85 * - Timestamp
86 * - Category
87 * - Severity
88 * - Message
89 * - JSON context
90 *
91 * Also:
92 * - Fires WordPress action hook
93 * - Updates error summary in wp_options
94 *
95 * @param string $category One of the CATEGORY_* constants
96 * @param string $severity One of the SEVERITY_* constants
97 * @param string $message Human-readable error message
98 * @param array $context Additional context data (optional)
99 * @return bool True on success, false on failure
100 */
101 public static function log($category, $severity, $message, $context = []) {
102 // Validate inputs
103 if (empty($category) || empty($severity) || empty($message)) {
104 return false;
105 }
106
107 // Get error code for this category
108 $error_code = self::$error_codes[$category] ?? 'MS-0000';
109
110 // Prepare full context with error code
111 $full_context = array_merge([
112 'error_code' => $error_code
113 ], $context);
114
115 // Format timestamp
116 $timestamp = date('Y-m-d H:i:s');
117
118 // Format log line: [YYYY-MM-DD HH:MM:SS] [CATEGORY] [SEVERITY] Message {context_json}
119 $log_line = sprintf(
120 "[%s] [%s] [%s] %s %s\n",
121 $timestamp,
122 $category,
123 $severity,
124 $message,
125 json_encode($full_context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
126 );
127
128 // Write to dedicated log file
129 $log_written = self::write_to_log_file($log_line);
130
131 // Fire WordPress action hook for external monitoring
132 do_action('metasync_error_logged', $category, $error_code, $message, $full_context);
133
134 // Update error summary in database
135 self::update_error_summary($category, $error_code, $message, $full_context);
136
137 return $log_written;
138 }
139
140 /**
141 * Write log line to dedicated error log file
142 *
143 * @param string $log_line Formatted log line
144 * @return bool True on success, false on failure
145 */
146 private static function write_to_log_file($log_line) {
147 // Get log directory (same as Log_Manager uses)
148 $log_directory = WP_CONTENT_DIR . '/metasync_data';
149
150 // Create directory if it doesn't exist
151 if (!is_dir($log_directory)) {
152 if (!@mkdir($log_directory, 0755, true)) {
153 error_log('Metasync_Error_Logger: Failed to create log directory: ' . $log_directory);
154 return false;
155 }
156 }
157
158 // Verify directory is writable
159 if (!is_writable($log_directory)) {
160 @chmod($log_directory, 0755);
161 if (!is_writable($log_directory)) {
162 error_log('Metasync_Error_Logger: Directory not writable: ' . $log_directory);
163 return false;
164 }
165 }
166
167 // Get log file path
168 $log_file = wp_normalize_path($log_directory . '/' . self::LOG_FILE_NAME);
169
170 // Append to log file with file locking
171 $result = @file_put_contents($log_file, $log_line, FILE_APPEND | LOCK_EX);
172
173 if ($result === false) {
174 error_log('Metasync_Error_Logger: Failed to write to log file: ' . $log_file);
175 return false;
176 }
177
178 // Set file permissions if file was just created
179 if (!file_exists($log_file) || filesize($log_file) === strlen($log_line)) {
180 @chmod($log_file, 0644);
181 }
182
183 return true;
184 }
185
186 /**
187 * Update error summary in wp_options
188 *
189 * Tracks last 100 unique errors with counts.
190 * Unique key is based on category + first 50 chars of message.
191 *
192 * @param string $category Error category
193 * @param string $code Error code
194 * @param string $message Error message
195 * @param array $context Error context
196 */
197 private static function update_error_summary($category, $code, $message, $context) {
198 // Get existing summary
199 $summary = get_option(self::ERROR_SUMMARY_OPTION, []);
200
201 if (!is_array($summary)) {
202 $summary = [];
203 }
204
205 // Create unique key from category + first 50 chars of message
206 $message_preview = substr($message, 0, 50);
207 $key = $category . '|' . $message_preview;
208
209 // Initialize entry if it doesn't exist
210 if (!isset($summary[$key])) {
211 $summary[$key] = [
212 'category' => $category,
213 'code' => $code,
214 'message' => $message,
215 'count' => 0,
216 'first_seen' => current_time('mysql'),
217 'last_seen' => current_time('mysql'),
218 'severity' => isset($context['severity']) ? $context['severity'] : 'UNKNOWN'
219 ];
220 }
221
222 // Update count and last seen time
223 $summary[$key]['count']++;
224 $summary[$key]['last_seen'] = current_time('mysql');
225
226 // Keep only last 100 unique errors (prune oldest)
227 if (count($summary) > self::MAX_SUMMARY_ENTRIES) {
228 // Sort by last_seen (newest first)
229 uasort($summary, function($a, $b) {
230 return strtotime($b['last_seen']) - strtotime($a['last_seen']);
231 });
232
233 // Keep only the most recent entries
234 $summary = array_slice($summary, 0, self::MAX_SUMMARY_ENTRIES, true);
235 }
236
237 // Save to database
238 update_option(self::ERROR_SUMMARY_OPTION, $summary);
239 }
240
241 /**
242 * Get error summary from wp_options
243 *
244 * @return array Error summary array
245 */
246 public static function get_error_summary() {
247 $summary = get_option(self::ERROR_SUMMARY_OPTION, []);
248 return is_array($summary) ? $summary : [];
249 }
250
251 /**
252 * Clear error summary
253 *
254 * @return bool True on success
255 */
256 public static function clear_error_summary() {
257 return delete_option(self::ERROR_SUMMARY_OPTION);
258 }
259
260 /**
261 * Get error code for a category
262 *
263 * @param string $category Error category
264 * @return string Error code or 'MS-0000' if not found
265 */
266 public static function get_error_code($category) {
267 return self::$error_codes[$category] ?? 'MS-0000';
268 }
269
270 /**
271 * Get all error codes mapping
272 *
273 * @return array Error codes array
274 */
275 public static function get_all_error_codes() {
276 return self::$error_codes;
277 }
278 }
279
280
281 /**
282 * Check Action Scheduler queue for overflow
283 * This checks if pending actions exceed 1000 and logs QUEUE_OVERFLOW error
284 *
285 * @param bool $force_check If true, bypasses transient throttling (for manual testing)
286 * @return int|false Returns pending count if checked, false if skipped
287 */
288 function metasync_check_action_scheduler_queue_overflow($force_check = false) {
289 // Only check if Action Scheduler is available
290 if (!class_exists('ActionScheduler_Store')) {
291 return false;
292 }
293
294 // Only check if Error Logger is available
295 if (!class_exists('Metasync_Error_Logger')) {
296 return false;
297 }
298
299 // Throttle: Only check once per hour to avoid excessive logging (unless forced)
300 if (!$force_check) {
301 $transient_key = 'metasync_queue_overflow_check';
302 $last_check = get_transient($transient_key);
303
304 if ($last_check !== false) {
305 return false; // Already checked recently
306 }
307 }
308
309 try {
310 $store = ActionScheduler_Store::instance();
311
312 // Get count of pending actions
313 $pending_count = (int) $store->query_actions([
314 'status' => ActionScheduler_Store::STATUS_PENDING,
315 'per_page' => 0, // We only need count
316 ], 'count');
317
318 // Check if queue overflow threshold is exceeded (>1000)
319 if ($pending_count > 1000) {
320 Metasync_Error_Logger::log(
321 Metasync_Error_Logger::CATEGORY_QUEUE_OVERFLOW,
322 Metasync_Error_Logger::SEVERITY_WARNING,
323 'Action Scheduler queue overflow - too many pending actions',
324 [
325 'pending_count' => $pending_count,
326 'threshold' => 1000,
327 'queue_system' => 'Action Scheduler',
328 'operation' => 'queue_processing'
329 ]
330 );
331 }
332
333 // Set transient to throttle future checks (1 hour) - only if not forced
334 if (!$force_check) {
335 $transient_key = 'metasync_queue_overflow_check';
336 set_transient($transient_key, time(), HOUR_IN_SECONDS);
337 }
338
339 return $pending_count;
340 } catch (Exception $e) {
341 // Fail silently to prevent breaking queue processing
342 // error_log('MetaSync: Failed to check queue overflow: ' . $e->getMessage());
343 return false;
344 }
345 }
346
347 // Hook into Action Scheduler before processing queue
348 add_action('action_scheduler_before_process_queue', 'metasync_check_action_scheduler_queue_overflow', 10, 0);
349
350 // Also check on shutdown (for immediate detection, throttled)
351 add_action('shutdown', function() {
352 // Only check in admin or if triggered manually
353 if (is_admin() || isset($_GET['metasync_check_queue'])) {
354 metasync_check_action_scheduler_queue_overflow();
355 }
356 }, 999);
357