$area ], $area ); } /** * Logger for the outbound webhook pipeline, scoped to a single topic. */ public static function by_webhook( string $topic ): Context_Logger { $topic = self::slug( $topic ); $prefix = sprintf( '[%s][%s][%s]', self::SOURCE_PREFIX, self::LOG_WEBHOOKS, $topic ); return new Context_Logger( $prefix, [ 'topic' => $topic, 'log_name' => self::LOG_WEBHOOKS, ], self::LOG_WEBHOOKS ); } public static function by_sale_channel( int $sale_channel_id, string $channel_type, string $log_name = self::LOG_SALE_CHANNELS ): Context_Logger { $channel_type = self::slug( $channel_type ); $log_name = self::slug( $log_name ); $prefix = sprintf( '[%s][%s-%d][%s]', self::SOURCE_PREFIX, $channel_type, $sale_channel_id, $log_name ); return new Context_Logger( $prefix, [ 'sale_channel_id' => $sale_channel_id, 'channel_type' => $channel_type, 'log_name' => $log_name, ], self::LOG_SALE_CHANNELS ); } private static function slug( string $value ): string { $slug = sanitize_key( str_replace( '_', '-', $value ) ); return '' === $slug ? 'general' : $slug; } } /** * Small contextual writer for the plugin's daily log. */ final class Context_Logger { private const REDACTED = '[REDACTED]'; private const SENSITIVE_KEY_PARTS = [ 'authorization', 'access-token', 'refresh-token', 'store-token', 'client-key', 'client-secret', 'password', 'secret', 'token', ]; public function __construct( private readonly string $prefix, private readonly array $base_context, private readonly string $channel ) {} public function emergency( string $message, array $context = [] ): void { $this->log( 'emergency', $message, $context ); } public function alert( string $message, array $context = [] ): void { $this->log( 'alert', $message, $context ); } public function critical( string $message, array $context = [] ): void { $this->log( 'critical', $message, $context ); } public function error( string $message, array $context = [] ): void { $this->log( 'error', $message, $context ); } public function warning( string $message, array $context = [] ): void { $this->log( 'warning', $message, $context ); } public function notice( string $message, array $context = [] ): void { $this->log( 'notice', $message, $context ); } public function info( string $message, array $context = [] ): void { $this->log( 'info', $message, $context ); } public function debug( string $message, array $context = [] ): void { $this->log( 'debug', $message, $context ); } public function log( string $level, string $message, array $context = [] ): void { // Operational levels always land in the plugin's own daily file; the // chatty debug level stays behind WP_DEBUG so production stays readable. if ( 'debug' === $level && ! self::is_debug_log_enabled() ) { return; } try { // Every line carries the file and line of its call site so a message can // be traced back to the code that emitted it, without each call site // having to repeat the location by hand. [ $file, $line ] = self::caller_location(); $message = sprintf( 'File: %s - Line: %s | %s', $file, $line, $message ); $context = array_merge( self::redact_context( $context ), $this->base_context ); $encoded = wp_json_encode( $context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( ! is_string( $encoded ) ) { $encoded = '{}'; } // Logging is best-effort. An unwritable log destination must never // interrupt the plugin's request flow. Log_File::append( sprintf( '[%s] %s %s %s CONTEXT: %s', gmdate( 'Y-m-d H:i:s' ), $this->prefix, strtoupper( $level ), $message, $encoded ), $this->channel ); } catch ( \Throwable ) { // Never allow optional diagnostics to break marketplace flows. } } /** * File (relative to the plugin) and line of the code that called the logger. * * The public level helpers forward to log(), so the outermost frame that * still belongs to this class is the real call site; its file and line point * at the caller. * * @return array{0:string,1:int} */ private static function caller_location(): array { $file = 'unknown'; $line = 0; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Used solely to identify the calling file and line for logger context. foreach ( debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 4 ) as $frame ) { if ( ( $frame['class'] ?? '' ) !== self::class ) { break; } $file = $frame['file'] ?? $file; $line = (int) ( $frame['line'] ?? $line ); } if ( defined( 'MARKET_SYNC_PLUGIN_DIR' ) && str_starts_with( $file, MARKET_SYNC_PLUGIN_DIR ) ) { $file = substr( $file, strlen( MARKET_SYNC_PLUGIN_DIR ) ); } return [ $file, $line ]; } private static function is_debug_log_enabled(): bool { $wp_debug = defined('WP_DEBUG') && constant('WP_DEBUG'); $wp_debug_log = defined('WP_DEBUG_LOG') && constant('WP_DEBUG_LOG'); return $wp_debug && $wp_debug_log; } private static function redact_context( array $context ): array { foreach ( $context as $key => $value ) { $normalized_key = str_replace( '_', '-', strtolower( (string) $key ) ); if ( self::is_sensitive_key( $normalized_key ) ) { $context[ $key ] = self::REDACTED; continue; } if ( is_array( $value ) ) { $context[ $key ] = self::redact_context( $value ); } } return $context; } private static function is_sensitive_key( string $key ): bool { foreach ( self::SENSITIVE_KEY_PARTS as $part ) { if ( str_contains( $key, $part ) ) { return true; } } return false; } }