PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.9
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.9
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.6.9, at includes/class-metasync-error-logger.php

367 lines 12.0 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 // Protect log directory from direct web access
159 $htaccess_file = $log_directory . '/.htaccess';
160 if (!file_exists($htaccess_file)) {
161 @file_put_contents($htaccess_file, "Order deny,allow\nDeny from all\n");
162 }
163 $index_file = $log_directory . '/index.php';
164 if (!file_exists($index_file)) {
165 @file_put_contents($index_file, "<?php\n// Silence is golden\n");
166 }
167
168 // Verify directory is writable
169 if (!is_writable($log_directory)) {
170 @chmod($log_directory, 0755);
171 if (!is_writable($log_directory)) {
172 error_log('Metasync_Error_Logger: Directory not writable: ' . $log_directory);
173 return false;
174 }
175 }
176
177 // Get log file path
178 $log_file = wp_normalize_path($log_directory . '/' . self::LOG_FILE_NAME);
179
180 // Append to log file with file locking
181 $result = @file_put_contents($log_file, $log_line, FILE_APPEND | LOCK_EX);
182
183 if ($result === false) {
184 error_log('Metasync_Error_Logger: Failed to write to log file: ' . $log_file);
185 return false;
186 }
187
188 // Set file permissions if file was just created
189 if (!file_exists($log_file) || filesize($log_file) === strlen($log_line)) {
190 @chmod($log_file, 0644);
191 }
192
193 return true;
194 }
195
196 /**
197 * Update error summary in wp_options
198 *
199 * Tracks last 100 unique errors with counts.
200 * Unique key is based on category + first 50 chars of message.
201 *
202 * @param string $category Error category
203 * @param string $code Error code
204 * @param string $message Error message
205 * @param array $context Error context
206 */
207 private static function update_error_summary($category, $code, $message, $context) {
208 // Get existing summary
209 $summary = get_option(self::ERROR_SUMMARY_OPTION, []);
210
211 if (!is_array($summary)) {
212 $summary = [];
213 }
214
215 // Create unique key from category + first 50 chars of message
216 $message_preview = substr($message, 0, 50);
217 $key = $category . '|' . $message_preview;
218
219 // Initialize entry if it doesn't exist
220 if (!isset($summary[$key])) {
221 $summary[$key] = [
222 'category' => $category,
223 'code' => $code,
224 'message' => $message,
225 'count' => 0,
226 'first_seen' => current_time('mysql'),
227 'last_seen' => current_time('mysql'),
228 'severity' => isset($context['severity']) ? $context['severity'] : 'UNKNOWN'
229 ];
230 }
231
232 // Update count and last seen time
233 $summary[$key]['count']++;
234 $summary[$key]['last_seen'] = current_time('mysql');
235
236 // Keep only last 100 unique errors (prune oldest)
237 if (count($summary) > self::MAX_SUMMARY_ENTRIES) {
238 // Sort by last_seen (newest first)
239 uasort($summary, function($a, $b) {
240 return strtotime($b['last_seen']) - strtotime($a['last_seen']);
241 });
242
243 // Keep only the most recent entries
244 $summary = array_slice($summary, 0, self::MAX_SUMMARY_ENTRIES, true);
245 }
246
247 // Save to database
248 update_option(self::ERROR_SUMMARY_OPTION, $summary);
249 }
250
251 /**
252 * Get error summary from wp_options
253 *
254 * @return array Error summary array
255 */
256 public static function get_error_summary() {
257 $summary = get_option(self::ERROR_SUMMARY_OPTION, []);
258 return is_array($summary) ? $summary : [];
259 }
260
261 /**
262 * Clear error summary
263 *
264 * @return bool True on success
265 */
266 public static function clear_error_summary() {
267 return delete_option(self::ERROR_SUMMARY_OPTION);
268 }
269
270 /**
271 * Get error code for a category
272 *
273 * @param string $category Error category
274 * @return string Error code or 'MS-0000' if not found
275 */
276 public static function get_error_code($category) {
277 return self::$error_codes[$category] ?? 'MS-0000';
278 }
279
280 /**
281 * Get all error codes mapping
282 *
283 * @return array Error codes array
284 */
285 public static function get_all_error_codes() {
286 return self::$error_codes;
287 }
288 }
289
290
291 /**
292 * Check Action Scheduler queue for overflow
293 * This checks if pending actions exceed 1000 and logs QUEUE_OVERFLOW error
294 *
295 * @param bool $force_check If true, bypasses transient throttling (for manual testing)
296 * @return int|false Returns pending count if checked, false if skipped
297 */
298 function metasync_check_action_scheduler_queue_overflow($force_check = false) {
299 // Only check if Action Scheduler is available
300 if (!class_exists('ActionScheduler_Store')) {
301 return false;
302 }
303
304 // Only check if Error Logger is available
305 if (!class_exists('Metasync_Error_Logger')) {
306 return false;
307 }
308
309 // Throttle: Only check once per hour to avoid excessive logging (unless forced)
310 if (!$force_check) {
311 $transient_key = 'metasync_queue_overflow_check';
312 $last_check = get_transient($transient_key);
313
314 if ($last_check !== false) {
315 return false; // Already checked recently
316 }
317 }
318
319 try {
320 $store = ActionScheduler_Store::instance();
321
322 // Get count of pending actions
323 $pending_count = (int) $store->query_actions([
324 'status' => ActionScheduler_Store::STATUS_PENDING,
325 'per_page' => 0, // We only need count
326 ], 'count');
327
328 // Check if queue overflow threshold is exceeded (>1000)
329 if ($pending_count > 1000) {
330 Metasync_Error_Logger::log(
331 Metasync_Error_Logger::CATEGORY_QUEUE_OVERFLOW,
332 Metasync_Error_Logger::SEVERITY_WARNING,
333 'Action Scheduler queue overflow - too many pending actions',
334 [
335 'pending_count' => $pending_count,
336 'threshold' => 1000,
337 'queue_system' => 'Action Scheduler',
338 'operation' => 'queue_processing'
339 ]
340 );
341 }
342
343 // Set transient to throttle future checks (1 hour) - only if not forced
344 if (!$force_check) {
345 $transient_key = 'metasync_queue_overflow_check';
346 set_transient($transient_key, time(), HOUR_IN_SECONDS);
347 }
348
349 return $pending_count;
350 } catch (Exception $e) {
351 // Fail silently to prevent breaking queue processing
352 // error_log('MetaSync: Failed to check queue overflow: ' . $e->getMessage());
353 return false;
354 }
355 }
356
357 // Hook into Action Scheduler before processing queue
358 add_action('action_scheduler_before_process_queue', 'metasync_check_action_scheduler_queue_overflow', 10, 0);
359
360 // Also check on shutdown (for immediate detection, throttled)
361 add_action('shutdown', function() {
362 // Only check in admin or if triggered manually
363 if (is_admin() || isset($_GET['metasync_check_queue'])) {
364 metasync_check_action_scheduler_queue_overflow();
365 }
366 }, 999);
367