From 8178a4f26f997c3389abc04c57da3135df9273a0 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:55:12 +0100 Subject: [PATCH] feat(logging): Refactor query event lifecycle for modularity. Added more lifecycle options --- plugins/wpgraphql-logging/src/Admin/.gitkeep | 0 .../Fields/Tab/Basic_Configuration_Tab.php | 27 +- plugins/wpgraphql-logging/src/Events/.gitkeep | 0 .../wpgraphql-logging/src/Events/Events.php | 97 +++-- .../src/Events/QueryActionLogger.php | 206 ++++++++++ .../src/Events/QueryEventLifecycle.php | 373 +++++------------- .../src/Events/QueryFilterLogger.php | 173 ++++++++ .../src/Logger/LoggingHelper.php | 59 +++ 8 files changed, 621 insertions(+), 314 deletions(-) delete mode 100644 plugins/wpgraphql-logging/src/Admin/.gitkeep delete mode 100644 plugins/wpgraphql-logging/src/Events/.gitkeep create mode 100644 plugins/wpgraphql-logging/src/Events/QueryActionLogger.php create mode 100644 plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php create mode 100644 plugins/wpgraphql-logging/src/Logger/LoggingHelper.php diff --git a/plugins/wpgraphql-logging/src/Admin/.gitkeep b/plugins/wpgraphql-logging/src/Admin/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php index bbf8555e..db58ea18 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php @@ -150,18 +150,21 @@ public function get_fields(): array { $fields[ self::EVENT_LOG_SELECTION ] = new Select_Field( - self::EVENT_LOG_SELECTION, - $this->get_name(), - __( 'Log Points', 'wpgraphql-logging' ), - [ - Events::PRE_REQUEST => __( 'Pre Request', 'wpgraphql-logging' ), - Events::BEFORE_GRAPHQL_EXECUTION => __( 'Before Query Execution', 'wpgraphql-logging' ), - Events::BEFORE_RESPONSE_RETURNED => __( 'Before Response Returned', 'wpgraphql-logging' ), - ], - '', - __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), - true - ); + self::EVENT_LOG_SELECTION, + $this->get_name(), + __( 'Log Points', 'wpgraphql-logging' ), + [ + Events::PRE_REQUEST => __( 'Pre Request', 'wpgraphql-logging' ), + Events::BEFORE_GRAPHQL_EXECUTION => __( 'Before Query Execution', 'wpgraphql-logging' ), + Events::BEFORE_RESPONSE_RETURNED => __( 'Before Response Returned', 'wpgraphql-logging' ), + Events::REQUEST_DATA => __( 'Request Data', 'wpgraphql-logging' ), + Events::REQUEST_RESULTS => __( 'Request Results', 'wpgraphql-logging' ), + Events::RESPONSE_HEADERS_TO_SEND => __( 'Response Headers', 'wpgraphql-logging' ), + ], + '', + __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), + true + ); return apply_filters( 'wpgraphql_logging_basic_configuration_fields', $fields ); } diff --git a/plugins/wpgraphql-logging/src/Events/.gitkeep b/plugins/wpgraphql-logging/src/Events/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/wpgraphql-logging/src/Events/Events.php b/plugins/wpgraphql-logging/src/Events/Events.php index 6a2094f8..602b8745 100644 --- a/plugins/wpgraphql-logging/src/Events/Events.php +++ b/plugins/wpgraphql-logging/src/Events/Events.php @@ -8,28 +8,75 @@ * List of available events that users can subscribe to with the EventManager. */ final class Events { - /** - * WPGraphQL action: do_graphql_request. - * - * Before the request is processed. - * - * @var string - */ - public const PRE_REQUEST = 'do_graphql_request'; - - /** - * WPGraphQL action: graphql_before_execute. - * - * @var string - */ - public const BEFORE_GRAPHQL_EXECUTION = 'graphql_before_execute'; - - /** - * WPGraphQL action: graphql_return_response - * - * Before the response is returned to the client. - * - * @var string - */ - public const BEFORE_RESPONSE_RETURNED = 'graphql_return_response'; -} + /** + * WPGraphQL action: do_graphql_request. + * + * Before the request is processed. + * + * @var string + */ + public const PRE_REQUEST = 'do_graphql_request'; + + /** + * WPGraphQL action: graphql_before_execute. + * + * @var string + */ + public const BEFORE_GRAPHQL_EXECUTION = 'graphql_before_execute'; + + /** + * WPGraphQL action: graphql_return_response + * + * Before the response is returned to the client. + * + * @var string + */ + public const BEFORE_RESPONSE_RETURNED = 'graphql_return_response'; + + /** + * WPGraphQL filter: graphql_request_data. + * + * Allows the request data to be filtered. Ideal for capturing the + * full payload before processing. + * + * @var string + */ + public const REQUEST_DATA = 'graphql_request_data'; + + /** + * WPGraphQL filter: graphql_response_headers_to_send. + * + * Filters the headers to send in the GraphQL response. + * + * @var string + */ + public const RESPONSE_HEADERS_TO_SEND = 'graphql_response_headers_to_send'; + + /** + * WPGraphQL filter: graphql_request_results. + * + * Filters the final results of the GraphQL execution. + * + * @var string + */ + public const REQUEST_RESULTS = 'graphql_request_results'; + + /** + * WPGraphQL filter: graphql_debug_enabled. + * + * Determines if GraphQL Debug is enabled. Useful for toggling logging. + * + * @var string + */ + public const DEBUG_ENABLED = 'graphql_debug_enabled'; + + /** + * WPGraphQL filter: graphql_app_context_config. + * + * Filters the config for the AppContext. Useful for storing temporary + * data for the duration of a request. + * + * @var string + */ + public const APP_CONTEXT_CONFIG = 'graphql_app_context_config'; +} \ No newline at end of file diff --git a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php new file mode 100644 index 00000000..1ebea818 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php @@ -0,0 +1,206 @@ +> + */ + protected array $config; + + /** + * QueryActionLogger constructor. + * + * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. + * @param array $config The logging configuration. + */ + public function __construct( LoggerService $logger, array $config ) { + $this->logger = $logger; + $this->config = $config; + } + + /** + * Initial Incoming Request. + * + * This method hooks into the `do_graphql_request` action. + * + * @param string $query + * @param string|null $operation_name + * @param array|null $variables + */ + public function log_pre_request( string $query, ?string $operation_name, ?array $variables ): void { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return; + } + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! in_array( Events::PRE_REQUEST, $selected_events, true ) ) { + return; + } + $context = [ + 'query' => $query, + 'variables' => $variables, + 'operation_name' => $operation_name, + ]; + $payload = EventManager::transform( Events::PRE_REQUEST, [ 'context' => $context, 'level' => Level::Info ] ); + $this->logger->log( $payload['level'], 'WPGraphQL Pre Request', $payload['context'] ); + EventManager::publish( Events::PRE_REQUEST, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::PRE_REQUEST, $e ); + } + } + + /** + * Before Request Execution. + * + * This method hooks into the `graphql_before_execute` action. + * + * @param Request $request + */ + public function log_graphql_before_execute( Request $request ): void { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return; + } + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! in_array( Events::BEFORE_GRAPHQL_EXECUTION, $selected_events, true ) ) { + return; + } + /** @var \GraphQL\Server\OperationParams $params */ + $params = $request->params; + $context = [ + 'query' => $params->query, + 'operation_name' => $params->operation, + 'variables' => $params->variables, + 'params' => $params, + ]; + $payload = EventManager::transform( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $context, 'level' => Level::Info ] ); + $this->logger->log( $payload['level'], 'WPGraphQL Before Query Execution', $payload['context'] ); + EventManager::publish( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::BEFORE_GRAPHQL_EXECUTION, $e ); + } + } + + /** + * Before the GraphQL response is returned to the client. + * + * This method hooks into the `graphql_return_response` action. + * + * @param array|\GraphQL\Executor\ExecutionResult $filtered_response + * @param array|\GraphQL\Executor\ExecutionResult $response + * @param WPSchema $schema + * @param string|null $operation + * @param string $query + * @param array|null $variables + * @param Request $request + * @param string|null $query_id + */ + public function log_before_response_returned( + array|ExecutionResult $filtered_response, + array|ExecutionResult $response, + WPSchema $schema, + ?string $operation, + string $query, + ?array $variables, + Request $request, + ?string $query_id + ): void { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return; + } + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! in_array( Events::BEFORE_RESPONSE_RETURNED, $selected_events, true ) ) { + return; + } + $context = [ + 'response' => $response, + 'schema' => $schema, + 'operation_name' => $operation, + 'query' => $query, + 'variables' => $variables, + 'request' => $request, + 'query_id' => $query_id, + ]; + $level = Level::Info; + $message = 'WPGraphQL Response'; + $errors = $this->get_response_errors( $response ); + if ( null !== $errors && count( $errors ) > 0 ) { + $context['errors'] = $errors; + $level = Level::Error; + $message = 'WPGraphQL Response with Errors'; + } + $payload = EventManager::transform( Events::BEFORE_RESPONSE_RETURNED, [ 'context' => $context, 'level' => $level ] ); + $this->logger->log( $payload['level'], $message, $payload['context'] ); + EventManager::publish( Events::BEFORE_RESPONSE_RETURNED, [ 'context' => $payload['context'] ] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::BEFORE_RESPONSE_RETURNED, $e ); + } + } + + /** + * Get the context for the response. + * + * @param array|\GraphQL\Executor\ExecutionResult $response The response. + * + * @return array|null + */ + protected function get_response_errors( array|ExecutionResult $response ): ?array { + if ( $response instanceof ExecutionResult && [] !== $response->errors ) { + return $response->errors; + } + + if ( ! is_array( $response ) ) { + return null; + } + + $errors = $response['errors'] ?? null; + if ( null === $errors || [] === $errors ) { + return null; + } + + return $errors; + } + + /** + * Handles and logs application errors. + * + * @param string $event + * @param \Throwable $exception + */ + protected function process_application_error( string $event, \Throwable $exception ): void { + error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore + } +} \ No newline at end of file diff --git a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php index 32c216f8..b4b6c62b 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php +++ b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php @@ -4,294 +4,113 @@ namespace WPGraphQL\Logging\Events; -use GraphQL\Executor\ExecutionResult; -use Monolog\Level; -use WPGraphQL\Logging\Logger\LoggerService; -use WPGraphQL\Request; -use WPGraphQL\WPSchema; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\Basic_Configuration_Tab; +use WPGraphQL\Logging\Logger\LoggerService; /** - * WPGraphQL Query Event Lifecycle. + * WPGraphQL Query Event Lifecycle Orchestrator. * - * Handles logging for GraphQL query lifecycle events. + * This class acts as a facade, orchestrating the logging of GraphQL query + * events by delegating responsibilities to specialized logger classes. * * @package WPGraphQL\Logging * * @since 0.0.1 */ class QueryEventLifecycle { - /** - * The logger service instance. - * - * @var \WPGraphQL\Logging\Logger\LoggerService - */ - protected LoggerService $logger; - - /** - * The single instance of the class. - * - * @var \WPGraphQL\Logging\Events\QueryEventLifecycle|null - */ - private static ?QueryEventLifecycle $instance = null; - - /** - * @param \WPGraphQL\Logging\Logger\LoggerService $logger - */ - protected function __construct( LoggerService $logger ) { - $this->logger = $logger; - $full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] ); - $this->config = $full_config['basic_configuration'] ?? []; - } - - /** - * Get or create the single instance of the class. - */ - public static function init(): QueryEventLifecycle { - if ( null === self::$instance ) { - $logger = LoggerService::get_instance(); - self::$instance = new self( $logger ); - self::$instance->setup(); - } - - return self::$instance; - } - - /** - * Checks if logging is enabled based on user settings. - */ - protected function is_logging_enabled(): bool { - // Check the main "Enabled" checkbox first. - $is_enabled = $this->config[ Basic_Configuration_Tab::ENABLED ] ?? false; - if ( ! $is_enabled ) { - return false; - } - - // Check if the current user is an admin if that option is enabled. - $log_for_admin = $this->config[ Basic_Configuration_Tab::ADMIN_USER_LOGGING ] ?? false; - if ( $log_for_admin && ! current_user_can( 'manage_options' ) ) { - return false; - } - - // Check for IP restrictions. - $ip_restrictions = $this->config[ Basic_Configuration_Tab::IP_RESTRICTIONS ] ?? ''; - if ( ! empty( $ip_restrictions ) ) { - $allowed_ips = array_map( 'trim', explode( ',', $ip_restrictions ) ); - if ( ! in_array( $_SERVER['REMOTE_ADDR'], $allowed_ips, true ) ) { - return false; - } + /** + * The single instance of the class. + * + * @var \WPGraphQL\Logging\Events\QueryEventLifecycle|null + */ + private static ?QueryEventLifecycle $instance = null; + + /** + * The logger service instance. + * + * @var \WPGraphQL\Logging\Logger\LoggerService + */ + protected LoggerService $logger; + + /** + * The basic configuration settings. + * + * @var array> + */ + protected array $config; + + /** + * The logger for handling WordPress action hooks. + * + * @var \WPGraphQL\Logging\Events\QueryActionLogger + */ + protected QueryActionLogger $action_logger; + + /** + * The logger for handling WordPress filter hooks. + * + * @var \WPGraphQL\Logging\Events\QueryFilterLogger + */ + protected QueryFilterLogger $filter_logger; + + /** + * QueryEventLifecycle constructor. + * + * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. + */ + protected function __construct( LoggerService $logger ) { + $this->logger = $logger; + $full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] ); + $this->config = $full_config['basic_configuration'] ?? []; + + // Initialize the specialized logger components. + $this->action_logger = new QueryActionLogger( $this->logger, $this->config ); + $this->filter_logger = new QueryFilterLogger( $this->logger, $this->config ); + } + + /** + * Get or create the single instance of the class. + * + * @return QueryEventLifecycle + */ + public static function init(): QueryEventLifecycle { + if ( null === self::$instance ) { + $logger = LoggerService::get_instance(); + self::$instance = new self( $logger ); + self::$instance->setup(); + } + + return self::$instance; + } + + /** + * Register actions and filters to log the query event lifecycle. + * + * @psalm-suppress HookNotFound + */ + protected function setup(): void { + // Map of action events to their corresponding logger methods and accepted args. + $action_events = [ + Events::PRE_REQUEST => [ 'method' => 'log_pre_request', 'accepted_args' => 3 ], + Events::BEFORE_GRAPHQL_EXECUTION => [ 'method' => 'log_graphql_before_execute', 'accepted_args' => 1 ], + Events::BEFORE_RESPONSE_RETURNED => [ 'method' => 'log_before_response_returned', 'accepted_args' => 8 ], + ]; + + // Map of filter events to their corresponding logger methods and accepted args. + $filter_events = [ + Events::REQUEST_DATA => [ 'method' => 'log_graphql_request_data', 'accepted_args' => 1 ], + Events::REQUEST_RESULTS => [ 'method' => 'log_graphql_request_results', 'accepted_args' => 7 ], + Events::RESPONSE_HEADERS_TO_SEND => [ 'method' => 'add_logging_headers', 'accepted_args' => 1 ], + ]; + + // Add action hooks. + foreach ( $action_events as $event_name => $data ) { + add_action( $event_name, [ $this->action_logger, $data['method'] ], 10, $data['accepted_args'] ); } - // Check the data sampling rate. - $sampling_rate = (int) ( $this->config[ Basic_Configuration_Tab::DATA_SAMPLING ] ?? 100 ); - if ( mt_rand( 0, 100 ) >= $sampling_rate ) { - return false; + // Add filter hooks. + foreach ( $filter_events as $event_name => $data ) { + add_filter( $event_name, [ $this->filter_logger, $data['method'] ], 10, $data['accepted_args'] ); } - - return true; - } - - /** - * Initial Incoming Request. - * - * @hook do_graphql_request - * - * @param string $query The GraphQL query string. - * @param string|null $operation_name The name of the operation. Made nullable. - * @param array|null $variables The variables for the query. Made nullable. - */ - public function log_pre_request( string $query, ?string $operation_name, ?array $variables ): void { - try { - if ( ! $this->is_logging_enabled() ) { - return; - } - - $context = [ - 'query' => $query, - 'variables' => $variables, - 'operation_name' => $operation_name, - ]; - - $payload = EventManager::transform( - Events::PRE_REQUEST, - [ - 'context' => $context, - 'level' => Level::Info, - ] - ); - - $this->logger->log( $payload['level'], 'WPGraphQL Pre Request', $payload['context'] ); - - EventManager::publish( - Events::PRE_REQUEST, - [ - 'context' => $payload['context'], - 'level' => (string) $payload['level']->getName(), - ] - ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::PRE_REQUEST, $e ); - } - } - - /** - * Before Request Execution. - * - * @hook graphql_before_execute - * - * @param \WPGraphQL\Request $request The WPGraphQL Request instance. - */ - public function log_graphql_before_execute(Request $request ): void { - try { - if ( ! $this->is_logging_enabled() ) { - return; - } - - /** @var \GraphQL\Server\OperationParams $params */ - $params = $request->params; - $context = [ - 'query' => $params->query, - 'operation_name' => $params->operation, - 'variables' => $params->variables, - 'params' => $params, - ]; - - $payload = EventManager::transform( - Events::BEFORE_GRAPHQL_EXECUTION, - [ - 'context' => $context, - 'level' => Level::Info, - ] - ); - - $this->logger->log( $payload['level'], 'WPGraphQL Before Query Execution', $payload['context'] ); - - EventManager::publish( - Events::BEFORE_GRAPHQL_EXECUTION, - [ - 'context' => $payload['context'], - 'level' => (string) $payload['level']->getName(), - ] - ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::BEFORE_GRAPHQL_EXECUTION, $e ); - } - } - - /** - * Before the GraphQL response is returned to the client. - * - * @hook graphql_return_response - * - * @param array|\GraphQL\Executor\ExecutionResult $filtered_response The filtered response for the GraphQL request. - * @param array|\GraphQL\Executor\ExecutionResult $response The response for the GraphQL request. - * @param \WPGraphQL\WPSchema $schema The schema object for the root request. - * @param string|null $operation The name of the operation. - * @param string $query The query that GraphQL executed. - * @param array|null $variables Variables passed to your GraphQL query. - * @param \WPGraphQL\Request $request Instance of the Request. - * @param string|null $query_id The query id that GraphQL executed. - */ - public function log_before_response_returned(array|ExecutionResult $filtered_response, array|ExecutionResult $response, WPSchema $schema, ?string $operation, string $query, ?array $variables, Request $request, ?string $query_id): void { - try { - if ( ! $this->is_logging_enabled() ) { - return; - } - $context = [ - 'response' => $response, - 'schema' => $schema, - 'operation_name' => $operation, - 'query' => $query, - 'variables' => $variables, - 'request' => $request, - 'query_id' => $query_id, - ]; - - $level = Level::Info; - $message = 'WPGraphQL Response'; - $errors = $this->get_response_errors( $response ); - if ( null !== $errors && count( $errors ) > 0 ) { - $context['errors'] = $errors; - $level = Level::Error; - $message = 'WPGraphQL Response with Errors'; - } - - $payload = EventManager::transform( - Events::BEFORE_RESPONSE_RETURNED, - [ - 'context' => $context, - 'level' => $level, - ] - ); - - $this->logger->log( $payload['level'], $message, $payload['context'] ); - - EventManager::publish( - Events::BEFORE_RESPONSE_RETURNED, - [ - 'context' => $payload['context'], - 'level' => (string) $payload['level']->getName(), - ] - ); - } catch ( \Throwable $e ) { - $this->process_application_error( Events::BEFORE_RESPONSE_RETURNED, $e ); - } - } - - /** - * Get the context for the response. - * - * @param array|\GraphQL\Executor\ExecutionResult $response The response. - * - * @return array|null - */ - protected function get_response_errors( array|ExecutionResult $response ): ?array { - if ( $response instanceof ExecutionResult && [] !== $response->errors ) { - return $response->errors; - } - - if ( ! is_array( $response ) ) { - return null; - } - - $errors = $response['errors'] ?? null; - if ( null === $errors || [] === $errors ) { - return null; - } - - return $errors; - } - - /** - * Register actions and filters to log the query event lifecycle. - * - * @psalm-suppress HookNotFound - */ - protected function setup(): void { - - /** - * Initial Incoming Request - */ - add_action( 'do_graphql_request', [ $this, 'log_pre_request' ], 10, 3 ); - - /** - * Before Query Execution - */ - add_action( 'graphql_before_execute', [ $this, 'log_graphql_before_execute' ], 10, 1 ); - - /** - * Response/Error Handling - */ - add_action( 'graphql_return_response', [ $this, 'log_before_response_returned' ], 10, 8 ); - } - - /** - * Processing application error when an exception is thrown. - * - * @param string $event The event name. - * @param \Throwable $exception The exception. - */ - protected function process_application_error(string $event, \Throwable $exception): void { - error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore } -} +} \ No newline at end of file diff --git a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php new file mode 100644 index 00000000..781b18ac --- /dev/null +++ b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php @@ -0,0 +1,173 @@ +> + */ + protected array $config; + + /** + * QueryFilterLogger constructor. + * + * @param \WPGraphQL\Logging\Logger\LoggerService $logger The logger instance. + * @param array $config The logging configuration. + */ + public function __construct( LoggerService $logger, array $config ) { + $this->logger = $logger; + $this->config = $config; + } + + /** + * Logs and returns the GraphQL request data. + * + * This method hooks into the `graphql_request_data` filter. + * + * @param array $query_data The raw GraphQL request data. + * @return array The filtered query data. + */ + public function log_graphql_request_data( array $query_data ): array { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return $query_data; + } + + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! in_array( Events::REQUEST_DATA, $selected_events, true ) ) { + return $query_data; + } + + $context = [ + 'query' => $query_data['query'] ?? null, + 'variables' => $query_data['variables'] ?? null, + 'operation_name' => $query_data['operationName'] ?? null, + ]; + + $payload = EventManager::transform( Events::REQUEST_DATA, ['context' => $context] ); + $this->logger->log( Level::Info, 'WPGraphQL Request Data', $payload['context'] ); + EventManager::publish( Events::REQUEST_DATA, ['context' => $payload['context']] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::REQUEST_DATA, $e ); + } + + return $query_data; + } + + /** + * Logs and returns the final GraphQL request results. + * + * This method hooks into the `graphql_request_results` filter. + * + * @param array|\GraphQL\Executor\ExecutionResult $response The final GraphQL response. + * @param \WPGraphQL\WPSchema $schema The GraphQL schema. + * @param string|null $operation The name of the operation being executed. + * @param string|null $query The raw GraphQL query string. + * @param array|null $variables The query variables. + * @param \WPGraphQL\Request $request The WPGraphQL request instance. + * @param string|null $query_id The unique ID of the query. + * @return array|\GraphQL\Executor\ExecutionResult The filtered response. + */ + public function log_graphql_request_results( + array|ExecutionResult $response, + \WPGraphQL\WPSchema $schema, + ?string $operation, + ?string $query, + ?array $variables, + Request $request, + ?string $query_id + ): array|ExecutionResult { + try { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return $response; + } + + $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; + if ( ! in_array( Events::REQUEST_RESULTS, $selected_events, true ) ) { + return $response; + } + + $context = [ + 'response' => $response, + 'operation_name' => $request->params->operation, + 'query' => $request->params->query, + 'variables' => $request->params->variables, + 'request' => $request, + ]; + + $level = Level::Info; + $message = 'WPGraphQL Response'; + if ( isset( $response['errors'] ) && ! empty( $response['errors'] ) ) { + $context['errors'] = $response['errors']; + $level = Level::Error; + $message = 'WPGraphQL Response with Errors'; + } + + $payload = EventManager::transform( Events::REQUEST_RESULTS, ['context' => $context, 'level' => $level] ); + $this->logger->log( $payload['level'], $message, $payload['context'] ); + EventManager::publish( Events::REQUEST_RESULTS, ['context' => $payload['context']] ); + } catch ( \Throwable $e ) { + $this->process_application_error( Events::REQUEST_RESULTS, $e ); + } + + return $response; + } + + /** + * Adds a unique logging ID to the GraphQL response headers. + * + * This method hooks into the `graphql_response_headers_to_send` filter. + * + * @param array $headers The array of response headers. + * @return array The filtered array of headers. + */ + public function add_logging_headers( array $headers ): array { + if ( ! $this->is_logging_enabled( $this->config ) ) { + return $headers; + } + + $request_id = uniqid( 'wpgql_log_' ); + $headers['X-WPGraphQL-Logging-ID'] = $request_id; + + return $headers; + } + + /** + * Handles and logs application errors. + * + * @param string $event The name of the event where the error occurred. + * @param \Throwable $exception The exception that was caught. + */ + protected function process_application_error( string $event, \Throwable $exception ): void { + error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore + } +} \ No newline at end of file diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php new file mode 100644 index 00000000..b61d2e03 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -0,0 +1,59 @@ + $config The logging configuration. + * @return bool + */ + protected function is_logging_enabled( array $config ): bool { + $is_enabled = true; + + // Check the main "Enabled" checkbox. + if ( ! ( $config[ Basic_Configuration_Tab::ENABLED ] ?? false ) ) { + $is_enabled = false; + } + + // Check if the current user is an admin if that option is enabled. + if ( $is_enabled && ( $config[ Basic_Configuration_Tab::ADMIN_USER_LOGGING ] ?? false ) ) { + if ( ! current_user_can( 'manage_options' ) ) { + $is_enabled = false; + } + } + + // Check for IP restrictions. + $ip_restrictions = $config[ Basic_Configuration_Tab::IP_RESTRICTIONS ] ?? ''; + if ( $is_enabled && ! empty( $ip_restrictions ) ) { + $allowed_ips = array_map( 'trim', explode( ',', $ip_restrictions ) ); + if ( ! in_array( $_SERVER['REMOTE_ADDR'], $allowed_ips, true ) ) { + $is_enabled = false; + } + } + + // Check the data sampling rate. + if ( $is_enabled ) { + $sampling_rate = (int) ( $config[ Basic_Configuration_Tab::DATA_SAMPLING ] ?? 100 ); + if ( mt_rand( 0, 100 ) >= $sampling_rate ) { + $is_enabled = false; + } + } + + /** + * Filter the final decision on whether to log a request. + * + * @param bool $is_enabled True if logging is enabled, false otherwise. + * @param array $config The current logging configuration. + */ + return apply_filters( 'wpgraphql_logging_is_enabled', $is_enabled, $config ); + } +} \ No newline at end of file