diff --git a/plugins/wpgraphql-logging/.gitignore b/plugins/wpgraphql-logging/.gitignore
index ab99ea50..f0bc5578 100644
--- a/plugins/wpgraphql-logging/.gitignore
+++ b/plugins/wpgraphql-logging/.gitignore
@@ -48,10 +48,11 @@ c3.php
# Cache
phpcs-cache.json
+.psalm-cache/
tests/_support/
tests/_output/
tests/_generated/
tests/_data/
# Playwright outputs
-artifacts
\ No newline at end of file
+artifacts
diff --git a/plugins/wpgraphql-logging/README.md b/plugins/wpgraphql-logging/README.md
index 84484323..f1d6ed0b 100644
--- a/plugins/wpgraphql-logging/README.md
+++ b/plugins/wpgraphql-logging/README.md
@@ -23,6 +23,7 @@ A WPGraphQL logging plugin that provides visibility into request lifecycle to he
- [Features](#features)
- [Usage](#usage)
- [Configuration](#configuration)
+- [Admin & Settings](#admin--settings)
- [Extending the Functionality](#extending-the-functionality)
- [Testing](#testing)
@@ -59,6 +60,7 @@ wpgraphql-logging/
├── docs/ # Docs for extending the plugin.
├── src/ # Main plugin source code
│ ├── Admin/ # Admin settings, menu, and settings page logic
+│ ├── Settings/ # Admin settings functionality for displaying and saving data.
│ ├── Events/ # Event logging, pub/sub event manager for extending the logging.
│ ├── Logging/ # Logging logic, logger service, Monolog handlers & processors
│ ├── Plugin.php # Main plugin class (entry point)
@@ -110,6 +112,9 @@ The following documentation is available in the `docs/` directory:
- [Logging](docs/Logging.md):
Learn about the logging system, Monolog integration, handlers, processors, and how to use or extend the logger.
+- [Admin](docs/admin.md):
+ Learn how the admin settings page works, all available hooks, and how to add tabs/fields via actions and filters.
+
---
@@ -126,6 +131,10 @@ The following documentation is available in the `docs/` directory:
---
+## Admin & Settings
+
+See `docs/admin.md` for a full overview of the admin/settings architecture, hooks, and examples for adding tabs and fields.
+
## Testing
See [Testing.md](TESTING.md) for details on how to test the plugin.
diff --git a/plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css b/plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css
new file mode 100644
index 00000000..556fc2e6
--- /dev/null
+++ b/plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css
@@ -0,0 +1,86 @@
+settings_page_wpgraphql-logging #poststuff .postbox .inside h2 {
+ font-size: 1.3em;
+ font-weight: 600;
+ padding-left: 0;
+}
+
+
+.form-table td input[type="text"] {
+ width: calc(99% - 24px);
+ display: inline-block;
+}
+.form-table td select {
+ width: calc(99% - 24px);
+ display: inline-block;
+}
+
+.wpgraphql-logging-tooltip {
+ position: relative;
+ vertical-align: middle;
+ display: inline-block;
+ margin-right: 0.25rem;
+}
+
+.wpgraphql-logging-tooltip .dashicons {
+ color: #787c82;
+ vertical-align: middle;
+}
+
+.wpgraphql-logging-tooltip .tooltip-text.description {
+ opacity: 0;
+ visibility: hidden;
+ text-align: center;
+ color: #fff;
+ background-color: #1d2327;
+ border-radius: 4px;
+ position: absolute;
+ z-index: 1;
+ width: 180px;
+ padding: 0.5rem;
+ top: 50%;
+ transform: translateY(-50%);
+ vertical-align: middle;
+ margin-left: 0.25rem;
+ transition: opacity 0.12s ease;
+}
+
+.wpgraphql-logging-tooltip .tooltip-text::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -10px;
+ border-width: 6px;
+ border-style: solid;
+ border-color: transparent #1d2327 transparent transparent;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.wpgraphql-logging-tooltip:hover .tooltip-text,
+.wpgraphql-logging-tooltip:focus-within .tooltip-text {
+ visibility: visible;
+ opacity: 1;
+}
+
+.wpgraphql-logging-docs ul li {
+ list-style-type: none;
+ margin-left: 30px;
+ padding-bottom: 16px;
+}
+
+.wpgraphql-logging-docs ul li:before {
+ content: url(../../icons/doc.svg);
+ height: 1em;
+ margin-left: -29px;
+ margin-top: -2px;
+ position: absolute;
+ width: 0.5em;
+}
+
+
+.wpgraphql-logging-feature-list {
+ list-style-type: disc;
+ font-size: 1.1em;
+ margin-left: 30px;
+ padding-bottom: 16px;
+}
diff --git a/plugins/wpgraphql-logging/assets/icons/doc.svg b/plugins/wpgraphql-logging/assets/icons/doc.svg
new file mode 100644
index 00000000..2bc45f26
--- /dev/null
+++ b/plugins/wpgraphql-logging/assets/icons/doc.svg
@@ -0,0 +1,3 @@
+
diff --git a/plugins/wpgraphql-logging/bin/local/setup-docker-env.sh b/plugins/wpgraphql-logging/bin/local/setup-docker-env.sh
old mode 100644
new mode 100755
diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json
index 801da78f..d4552747 100644
--- a/plugins/wpgraphql-logging/composer.json
+++ b/plugins/wpgraphql-logging/composer.json
@@ -143,9 +143,8 @@
"phpstan": [
"vendor/bin/phpstan analyze --ansi --memory-limit=1G"
],
- "php:psalm": "psalm",
- "php:psalm:info": "psalm --show-info=true",
- "php:psalm:fix": "psalm --alter",
+ "php:psalm": "psalm --output-format=text --no-progress",
+ "php:psalm:fix": "psalm --alter --output-format=text --no-progress",
"qa": "sh bin/local/run-qa.sh",
"test": [
"sh bin/local/run-unit-tests.sh coverage",
diff --git a/plugins/wpgraphql-logging/docs/admin.md b/plugins/wpgraphql-logging/docs/admin.md
new file mode 100644
index 00000000..09e35b47
--- /dev/null
+++ b/plugins/wpgraphql-logging/docs/admin.md
@@ -0,0 +1,193 @@
+### Admin and Settings
+
+This document explains how the WPGraphQL Logging admin settings UI is built and how to extend it with your own tabs and fields.
+
+---
+
+## Architecture Overview
+
+- **Settings page entry**: `WPGraphQL\Logging\Admin\Settings_Page`
+ - Registers the submenu page and orchestrates fields and tabs
+ - Hooks added: `init` (init fields), `admin_menu` (page), `admin_init` (fields), `admin_enqueue_scripts` (assets)
+- **Menu page**: `WPGraphQL\Logging\Admin\Settings\Menu\Menu_Page`
+ - Adds a submenu under Settings → WPGraphQL Logging (`wpgraphql-logging`)
+ - Renders template `src/Admin/Settings/Templates/admin.php`
+- **Form manager**: `WPGraphQL\Logging\Admin\Settings\Settings_Form_Manager`
+ - Registers the settings (`register_setting`) and sections/fields per tab
+ - Sanitizes and saves values per tab; unknown fields are pruned
+- **Field collection**: `WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Collection`
+ - Holds all tabs and fields. A default `Basic_Configuration_Tab` is registered
+- **Tabs**: Implement `Settings_Tab_Interface` with `get_name()`, `get_label()`, `get_fields()`
+- **Fields**: Implement `Settings_Field_Interface` or use built-ins:
+ - `Field\Checkbox_Field`
+ - `Field\Text_Input_Field`
+ - `Field\Select_Field`
+
+Settings are stored in an array option. Keys are filterable:
+
+- Option key: `wpgraphql_logging_settings` (filter `wpgraphql_logging_settings_group_option_key`)
+- Settings group: `wpgraphql_logging_settings_group` (filter `wpgraphql_logging_settings_group_settings_group`)
+
+To read values at runtime, use `WPGraphQL\Logging\Admin\Settings\Logging_Settings_Service`:
+
+```php
+use WPGraphQL\Logging\Admin\Settings\Logging_Settings_Service;
+
+$settings = new Logging_Settings_Service();
+$enabled = $settings->get_setting('basic_configuration', 'enabled', false);
+```
+
+---
+
+## Hooks Reference (Admin)
+
+- Action: `wpgraphql_logging_settings_init( Settings_Page $instance )`
+ - Fired after the settings page is initialized
+- Action: `wpgraphql_logging_settings_field_collection_init( Settings_Field_Collection $collection )`
+ - Fired after default tabs/fields are registered; primary extension point to add tabs/fields
+- Action: `wpgraphql_logging_settings_form_manager_init( Settings_Form_Manager $manager )`
+ - Fired when the form manager is constructed
+- Filter: `wpgraphql_logging_settings_group_option_key( string $option_key )`
+ - Change the option key used to store settings
+- Filter: `wpgraphql_logging_settings_group_settings_group( string $group )`
+ - Change the settings group name used in `register_setting`
+
+- Filter: `wpgraphql_logging_basic_configuration_fields( array $fields )`
+ - Modify the default fields rendered in the `basic_configuration` tab. You can add, remove, or replace fields by returning a modified associative array of `field_id => Settings_Field_Interface`.
+ - Example:
+ ```php
+ use WPGraphQL\Logging\Admin\Settings\Fields\Field\Checkbox_Field;
+
+ add_filter('wpgraphql_logging_basic_configuration_fields', function(array $fields): array {
+ // Add a custom toggle into the Basic Configuration tab
+ $fields['enable_feature_x'] = new Checkbox_Field(
+ 'enable_feature_x',
+ 'basic_configuration',
+ 'Enable Feature X',
+ '',
+ 'Turn on extra logging for Feature X.'
+ );
+
+ // Optionally remove an existing field
+ // unset($fields[ WPGraphQL\Logging\Admin\Settings\Fields\Tab\Basic_Configuration_Tab::DATA_SAMPLING ]);
+
+ return $fields;
+ });
+ ```
+
+Related (non-admin) hooks for context:
+
+- Action: `wpgraphql_logging_init( Plugin $instance )` (plugin initialized)
+- Action: `wpgraphql_logging_activate` / `wpgraphql_logging_deactivate`
+
+---
+
+## Add a New Tab
+
+Create a tab class implementing `Settings_Tab_Interface` and register it during `wpgraphql_logging_settings_field_collection_init`.
+
+```php
+ new Text_Input_Field(
+ 'my_setting',
+ $this->get_name(),
+ 'My Setting',
+ '',
+ 'Describe what this setting does.',
+ 'e.g., value'
+ ),
+ ];
+ }
+}
+
+add_action('wpgraphql_logging_settings_field_collection_init', function (Settings_Field_Collection $collection): void {
+ $collection->add_tab(new My_Custom_Tab());
+});
+```
+
+Notes:
+
+- `get_name()` must be a unique slug; it is used in the admin page URL (`tab` query arg) and section IDs
+- Fields returned by `get_fields()` must set their `tab` to this slug so they render on the tab
+
+---
+
+## Add a Field to an Existing Tab
+
+You can add fields directly to the shared field collection. Ensure the field’s `tab` matches the target tab name.
+
+```php
+add_field(
+ 'enable_feature_x',
+ new Checkbox_Field(
+ 'enable_feature_x',
+ 'basic_configuration', // target the built-in Basic Configuration tab
+ 'Enable Feature X',
+ '',
+ 'Turn on extra logging for Feature X.'
+ )
+ );
+});
+```
+
+Tips:
+
+- Only fields present in the collection are saved; unknown keys are pruned during sanitize
+- Field input names follow: `{$option_key}[{$tab}][{$field_id}]`
+
+---
+
+## Reading/Saving Behavior
+
+- Each submit saves only the current tab’s fields
+- Sanitization is delegated to each field via `sanitize_field($value)`
+- Unknown fields or tabs are ignored/pruned
+
+Example of reading a value elsewhere:
+
+```php
+use WPGraphQL\Logging\Admin\Settings\Logging_Settings_Service;
+
+$settings = new Logging_Settings_Service();
+$thresholdSeconds = (float) $settings->get_setting('basic_configuration', 'performance_metrics', '0');
+```
+
+---
+
+## Common Use Cases
+
+- Add organization-specific logging toggles (privacy, PII redaction)
+- Integrate with other plugins by exposing their settings under a new tab
+- Provide presets for log points (e.g., only log slow queries) via a custom select field
+
+---
+
+## Admin Page Details
+
+- Menu: Settings → WPGraphQL Logging (`admin.php?page=wpgraphql-logging`)
+- Tabs: `admin.php?page=wpgraphql-logging&tab={tab_slug}`
+- Sections and fields are rendered with `do_settings_sections('wpgraphql-logging-{tab_slug}')`
diff --git a/plugins/wpgraphql-logging/phpcs.xml b/plugins/wpgraphql-logging/phpcs.xml
index c5ad6dde..82c557f3 100644
--- a/plugins/wpgraphql-logging/phpcs.xml
+++ b/plugins/wpgraphql-logging/phpcs.xml
@@ -326,7 +326,7 @@
-
+
diff --git a/plugins/wpgraphql-logging/phpstan.neon.dist b/plugins/wpgraphql-logging/phpstan.neon.dist
index 05f71e10..58fb14d1 100644
--- a/plugins/wpgraphql-logging/phpstan.neon.dist
+++ b/plugins/wpgraphql-logging/phpstan.neon.dist
@@ -30,3 +30,7 @@ parameters:
paths:
- wpgraphql-logging.php
- src/
+ ignoreErrors:
+ - identifier: empty.notAllowed
+ -
+ message: '#Constant WPGRAPHQL_LOGGING.* not found\.#'
diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml
index 2b45895f..1bfaf14c 100644
--- a/plugins/wpgraphql-logging/psalm.xml
+++ b/plugins/wpgraphql-logging/psalm.xml
@@ -8,6 +8,7 @@
findUnusedBaselineEntry="true"
findUnusedCode="false"
phpVersion="8.1"
+ cacheDirectory=".psalm-cache"
>
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php
new file mode 100644
index 00000000..f42c0276
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php
@@ -0,0 +1,145 @@
+id;
+ }
+
+ /**
+ * Whether the field should be rendered for a specific tab.
+ *
+ * @param string $tab_key The tab key.
+ */
+ public function should_render_for_tab( string $tab_key ): bool {
+ return $tab_key === $this->tab;
+ }
+
+ /**
+ * Register the settings field.
+ *
+ * @param string $section The section ID.
+ * @param string $page The page URI.
+ * @param array $args The field arguments.
+ */
+ public function add_settings_field( string $section, string $page, array $args ): void {
+ /** @psalm-suppress InvalidArgument */
+ add_settings_field(
+ $this->get_id(),
+ $this->title,
+ [ $this, 'render_field_callback' ],
+ $page,
+ $section,
+ array_merge(
+ $args,
+ [
+ 'class' => $this->css_class,
+ 'description' => $this->description,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Callback function to render the field.
+ *
+ * @param array $args The field arguments.
+ */
+ public function render_field_callback( array $args ): void {
+ $tab_key = (string) ( $args['tab_key'] ?? '' );
+ $settings_key = (string) ( $args['settings_key'] ?? '' );
+
+ $option_value = (array) get_option( $settings_key, [] );
+
+ $id = $this->get_field_name( $settings_key, $tab_key, $this->get_id() );
+
+ printf(
+ '
+
+ %1$s
+ ',
+ esc_attr( $this->description ),
+ esc_attr( $id ),
+ );
+
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_field method handles escaping internally
+ echo $this->render_field( $option_value, $settings_key, $tab_key );
+ }
+
+ /**
+ * Generate a field name for form inputs.
+ *
+ * @param string $settings_key The settings key.
+ * @param string $tab_key The tab key.
+ * @param string $field_id The field ID.
+ */
+ protected function get_field_name( string $settings_key, string $tab_key, string $field_id ): string {
+ return "{$settings_key}[{$tab_key}][{$field_id}]";
+ }
+
+ /**
+ * Get the current field value.
+ *
+ * @param array $option_value The option value.
+ * @param string $tab_key The tab key.
+ * @param mixed $default_value The default value.
+ */
+ protected function get_field_value( array $option_value, string $tab_key, $default_value = '' ): mixed {
+ if ( ! array_key_exists( $tab_key, $option_value ) ) {
+ return $default_value;
+ }
+
+ /** @var array $tab_value */
+ $tab_value = $option_value[ $tab_key ]; // @phpstan-ignore varTag.nativeType
+ $id = $this->get_id();
+ if ( empty( $id ) ) {
+ return $default_value;
+ }
+
+ if ( ! array_key_exists( $id, $tab_value ) ) {
+ return $default_value;
+ }
+
+ $field_value = $tab_value[ $id ];
+
+ if ( is_null( $field_value ) ) {
+ return $default_value;
+ }
+ return $field_value;
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php
new file mode 100644
index 00000000..ba3a5f13
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php
@@ -0,0 +1,49 @@
+ $option_value The option value.
+ * @param string $setting_key The setting key.
+ * @param string $tab_key The tab key.
+ *
+ * @return string The rendered field HTML.
+ */
+ public function render_field( array $option_value, string $setting_key, string $tab_key ): string {
+ $field_name = $this->get_field_name( $setting_key, $tab_key, $this->get_id() );
+ $field_value = $this->get_field_value( $option_value, $tab_key, false );
+ $is_checked = is_bool( $field_value ) ? $field_value : false;
+
+ return sprintf(
+ '',
+ esc_attr( $field_name ),
+ checked( 1, $is_checked, false ),
+ sanitize_html_class( $this->css_class )
+ );
+ }
+
+ /**
+ * Sanitize the checkbox field value.
+ *
+ * @param mixed $value The field value to sanitize.
+ *
+ * @return bool The sanitized boolean value.
+ */
+ public function sanitize_field( $value ): bool {
+ return (bool) $value;
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php
new file mode 100644
index 00000000..1f25b812
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php
@@ -0,0 +1,125 @@
+ $options The options for the select field.
+ * @param string $css_class The settings field class.
+ * @param string $description The description field to show in the tooltip.
+ * @param bool $multiple Whether multiple selections are allowed.
+ *
+ * @phpstan-ignore-next-line constructor.missingParentCall
+ */
+ public function __construct(
+ readonly string $id,
+ readonly string $tab,
+ readonly string $title,
+ readonly array $options,
+ readonly string $css_class = '',
+ readonly string $description = '',
+ readonly bool $multiple = false
+ ) {
+ }
+
+ /**
+ * Render the select field.
+ *
+ * @param array $option_value The option value.
+ * @param string $setting_key The setting key.
+ * @param string $tab_key The tab key.
+ *
+ * @return string The rendered field HTML.
+ */
+ public function render_field( array $option_value, string $setting_key, string $tab_key ): string {
+ $field_name = $this->get_field_name( $setting_key, $tab_key, $this->get_id() );
+ $field_value = $this->get_field_value( $option_value, $tab_key, $this->multiple ? [] : '' );
+
+ // Ensure we have the correct format for comparison.
+ $selected_values = $this->multiple ? (array) $field_value : [ (string) $field_value ];
+
+ $html = '';
+
+ return $html;
+ }
+
+ /**
+ * Sanitize the select field value.
+ *
+ * @param mixed $value The field value to sanitize.
+ *
+ * @return mixed The sanitized value.
+ */
+ public function sanitize_field( $value ): mixed {
+ if ( ! $this->multiple ) {
+ return $this->sanitize_single_value( $value );
+ }
+
+ return $this->sanitize_multiple_value( (array) $value );
+ }
+
+ /**
+ * Sanitize a single value.
+ *
+ * @param string $value The value to sanitize.
+ *
+ * @return string The sanitized value.
+ */
+ protected function sanitize_single_value( $value ): string {
+ $sanitized_value = sanitize_text_field( (string) $value );
+ return array_key_exists( $sanitized_value, $this->options ) ? $sanitized_value : '';
+ }
+
+ /**
+ * Sanitize a multiple value.
+ *
+ * @param array $values The values to sanitize.
+ *
+ * @return array The sanitized values.
+ */
+ protected function sanitize_multiple_value( array $values ): array {
+ $sanitized = [];
+ foreach ( $values as $value ) {
+ $single = $this->sanitize_single_value( $value );
+ if ( '' !== $single ) {
+ $sanitized[] = $single;
+ }
+ }
+ return $sanitized;
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php
new file mode 100644
index 00000000..2c4d79ad
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php
@@ -0,0 +1,92 @@
+ $option_value The option value.
+ * @param string $setting_key The setting key.
+ * @param string $tab_key The tab key.
+ *
+ * @return string The rendered field HTML.
+ */
+ public function render_field( array $option_value, string $setting_key, string $tab_key ): string {
+ $field_name = $this->get_field_name( $setting_key, $tab_key, $this->get_id() );
+ $field_value = $this->get_field_value( $option_value, $tab_key, $this->default_value );
+
+
+ return sprintf(
+ '',
+ esc_attr( $this->get_input_type() ),
+ esc_attr( $field_name ),
+ esc_attr( $field_value ),
+ esc_attr( $this->placeholder ),
+ esc_attr( $this->css_class )
+ );
+ }
+
+ /**
+ * Sanitize the text input field value.
+ *
+ * @param mixed $value The field value to sanitize.
+ *
+ * @return string The sanitized string value.
+ */
+ public function sanitize_field( $value ): string {
+ if ( 'email' === $this->get_input_type() ) {
+ return sanitize_email( (string) $value );
+ }
+
+ if ( 'url' === $this->get_input_type() ) {
+ return esc_url_raw( (string) $value );
+ }
+
+ return sanitize_text_field( (string) $value );
+ }
+
+ /**
+ * Get the input type.
+ *
+ * @return string The input type.
+ */
+ protected function get_input_type(): string {
+ return 'text';
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php
new file mode 100644
index 00000000..ead2bbfe
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php
@@ -0,0 +1,109 @@
+
+ */
+ protected array $fields = [];
+
+ /**
+ * Array of tabs
+ *
+ * @var array<\WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface>
+ */
+ protected array $tabs = [];
+
+ /**
+ * Constructor to initialize the fields.
+ */
+ public function __construct() {
+ $this->add_tab( new Basic_Configuration_Tab() );
+ do_action( 'wpgraphql_logging_settings_field_collection_init', $this );
+ }
+
+ /**
+ * @return array<\WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface>
+ */
+ public function get_fields(): array {
+ return $this->fields;
+ }
+
+ /**
+ * Get a specific field by its key.
+ *
+ * @param string $key The key of the field to retrieve.
+ *
+ * @return \WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface|null The field if found, null otherwise.
+ */
+ public function get_field( string $key ): ?Settings_Field_Interface {
+ return $this->fields[ $key ] ?? null;
+ }
+
+ /**
+ * Add a field to the collection.
+ *
+ * @param string $key The key for the field.
+ * @param \WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface $field The field to add.
+ */
+ public function add_field( string $key, Settings_Field_Interface $field ): void {
+ $this->fields[ $key ] = $field;
+ }
+
+ /**
+ * Remove a field from the collection.
+ *
+ * @param string $key The key of the field to remove.
+ */
+ public function remove_field( string $key ): void {
+ unset( $this->fields[ $key ] );
+ }
+
+ /**
+ * Add a tab to the collection.
+ *
+ * @param \WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface $tab The tab to add.
+ */
+ public function add_tab( Settings_Tab_Interface $tab ): void {
+ $this->tabs[ $tab->get_name() ] = $tab;
+
+ foreach ( $tab->get_fields() as $field_key => $field ) {
+ $this->add_field( $field_key, $field );
+ }
+ }
+
+ /**
+ * Get all tabs.
+ *
+ * @return array<\WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface>
+ */
+ public function get_tabs(): array {
+ return $this->tabs;
+ }
+
+ /**
+ * Get a specific tab by its name.
+ *
+ * @param string $tab_name The name of the tab to retrieve.
+ *
+ * @return \WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface|null The tab if found, null otherwise.
+ */
+ public function get_tab( string $tab_name ): ?Settings_Tab_Interface {
+ return $this->tabs[ $tab_name ] ?? null;
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php
new file mode 100644
index 00000000..0332e3b0
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php
@@ -0,0 +1,51 @@
+ $option_value The option value.
+ * @param string $setting_key The setting key.
+ * @param string $tab_key The tab key.
+ */
+ public function render_field( array $option_value, string $setting_key, string $tab_key ): string;
+
+ /**
+ * Get the field ID
+ */
+ public function get_id(): string;
+
+ /**
+ * Whether the field should be rendered for a specific tab
+ *
+ * @param string $tab_key The tab key.
+ */
+ public function should_render_for_tab( string $tab_key ): bool;
+
+ /**
+ * Add the settings field
+ *
+ * @param string $section The section ID.
+ * @param string $page The page ID.
+ * @param array $args The field arguments.
+ */
+ public function add_settings_field( string $section, string $page, array $args ): void;
+
+ /**
+ * Sanitize field value
+ *
+ * @param mixed $value
+ */
+ public function sanitize_field( $value ): mixed;
+}
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
new file mode 100644
index 00000000..bbf8555e
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php
@@ -0,0 +1,168 @@
+ Array of fields keyed by field ID.
+ */
+ public function get_fields(): array {
+ $fields = [];
+
+ $fields[ self::ENABLED ] = new Checkbox_Field(
+ self::ENABLED,
+ $this->get_name(),
+ __( 'Enabled', 'wpgraphql-logging' ),
+ '',
+ __( 'Enable or disable WPGraphQL logging.', 'wpgraphql-logging' ),
+ );
+
+ $fields[ self::IP_RESTRICTIONS ] = new Text_Input_Field(
+ self::IP_RESTRICTIONS,
+ $this->get_name(),
+ __( 'IP Restrictions', 'wpgraphql-logging' ),
+ '',
+ __( 'Comma-separated list of IPv4/IPv6 addresses to restrict logging to. Leave empty to log from all IPs.', 'wpgraphql-logging' ),
+ __( 'e.g., 192.168.1.1, 10.0.0.1', 'wpgraphql-logging' )
+ );
+
+ $fields[ self::ADMIN_USER_LOGGING ] = new Checkbox_Field(
+ self::ADMIN_USER_LOGGING,
+ $this->get_name(),
+ __( 'Log only for admin users', 'wpgraphql-logging' ),
+ '',
+ __( 'Log only for admin users.', 'wpgraphql-logging' )
+ );
+
+ $fields[ self::WPGRAPHQL_FILTERING ] = new Text_Input_Field(
+ self::WPGRAPHQL_FILTERING,
+ $this->get_name(),
+ __( 'WPGraphQL Query Filtering', 'wpgraphql-logging' ),
+ '',
+ __( 'Comma-separated list of query names or patterns to log. Leave empty to log all queries.', 'wpgraphql-logging' ),
+ __( 'e.g., GetPost, GetPosts, introspection', 'wpgraphql-logging' )
+ );
+
+ $fields[ self::DATA_SAMPLING ] = new Select_Field(
+ self::DATA_SAMPLING,
+ $this->get_name(),
+ __( 'Data Sampling Rate', 'wpgraphql-logging' ),
+ [
+ '100' => __( '100% (All requests)', 'wpgraphql-logging' ),
+ '50' => __( '50% (Every other request)', 'wpgraphql-logging' ),
+ '25' => __( '25% (Every 4th request)', 'wpgraphql-logging' ),
+ '10' => __( '10% (Every 10th request)', 'wpgraphql-logging' ),
+ ],
+ '',
+ __( 'Percentage of requests to log for performance optimization.', 'wpgraphql-logging' ),
+ false
+ );
+
+ $fields[ self::PERFORMANCE_METRICS ] = new Text_Input_Field(
+ self::PERFORMANCE_METRICS,
+ $this->get_name(),
+ __( 'Performance Threshold (seconds)', 'wpgraphql-logging' ),
+ '',
+ __( 'Only log requests that take longer than this threshold. 0 logs all requests. Calculated in seconds.', 'wpgraphql-logging' ),
+ __( 'e.g., 1.5', 'wpgraphql-logging' )
+ );
+
+
+ $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
+ );
+
+ return apply_filters( 'wpgraphql_logging_basic_configuration_fields', $fields );
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php
new file mode 100644
index 00000000..7c102479
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php
@@ -0,0 +1,38 @@
+ Array of fields keyed by field ID.
+ */
+ public function get_fields(): array;
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php b/plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php
new file mode 100644
index 00000000..833d447d
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php
@@ -0,0 +1,95 @@
+
+ */
+ protected array $settings_values = [];
+
+ /**
+ * Initialize the settings service.
+ */
+ public function __construct() {
+ $this->setup();
+ }
+
+ /**
+ * Get the settings values.
+ *
+ * @return array
+ */
+ public function get_settings_values(): array {
+ return $this->settings_values;
+ }
+
+ /**
+ * Get the configuration for a specific tab.
+ *
+ * @param string $tab_key The tab key.
+ *
+ * @return array|null
+ */
+ public function get_tab_config( string $tab_key ): ?array {
+ return $this->settings_values[ $tab_key ] ?? null;
+ }
+
+ /**
+ * Get a specific setting value from a tab.
+ *
+ * @param string $tab_key The tab key.
+ * @param string $setting_key The setting key.
+ * @param mixed $default_value The default value if not found.
+ */
+ public function get_setting( string $tab_key, string $setting_key, $default_value = null ): mixed {
+ $tab_config = $this->get_tab_config( $tab_key );
+ return $tab_config[ $setting_key ] ?? $default_value;
+ }
+
+ /**
+ * The option key for the settings group.
+ */
+ public static function get_option_key(): string {
+ return (string) apply_filters( 'wpgraphql_logging_settings_group_option_key', WPGRAPHQL_LOGGING_SETTINGS_KEY );
+ }
+
+ /**
+ * The settings group for the options.
+ */
+ public static function get_settings_group(): string {
+ return (string) apply_filters( 'wpgraphql_logging_settings_group_settings_group', WPGRAPHQL_LOGGING_SETTINGS_GROUP );
+ }
+
+ /**
+ * Set up the settings values by retrieving them from the database or cache.
+ * This method is called in the constructor to ensure settings are available.
+ */
+ protected function setup(): void {
+ $option_key = self::get_option_key();
+ $settings_group = self::get_settings_group();
+
+ $value = wp_cache_get( $option_key, $settings_group );
+ if ( is_array( $value ) ) {
+ $this->settings_values = $value;
+
+ return;
+ }
+
+ $this->settings_values = (array) get_option( $option_key, [] );
+ wp_cache_set( $option_key, $this->settings_values, $settings_group );
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php b/plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php
new file mode 100644
index 00000000..99e5052e
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php
@@ -0,0 +1,109 @@
+>
+ */
+ protected array $args;
+
+ /**
+ * Constructor.
+ *
+ * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected.
+ * @param string $menu_title The text to be used for the menu.
+ * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu and only include lowercase alphanumeric, dashes, and underscores characters to be compatible with sanitize_key().
+ * @param string $template The template that will be included in the callback.
+ * @param array> $args The args array for the template.
+ */
+ public function __construct(
+ string $page_title,
+ string $menu_title,
+ string $menu_slug,
+ string $template,
+ array $args = []
+ ) {
+ $this->page_title = $page_title;
+ $this->menu_title = $menu_title;
+ $this->menu_slug = $menu_slug;
+ $this->template = $template;
+ $this->args = $args;
+ }
+
+ /**
+ * Registers the menu page in the WordPress admin.
+ */
+ public function register_page(): void {
+ add_submenu_page(
+ 'options-general.php',
+ $this->page_title,
+ $this->menu_title,
+ 'manage_options',
+ $this->menu_slug,
+ [ $this, 'registration_callback' ]
+ );
+ }
+
+ /**
+ * Callback function to display the content of the menu page.
+ */
+ public function registration_callback(): void {
+ if ( empty( $this->template ) || ! file_exists( $this->template ) ) {
+ printf(
+ '',
+ esc_html__( 'The WPGraphQL Logging Settings template does not exist.', 'wpgraphql-logging' )
+ );
+
+ return;
+ }
+ foreach ( $this->args as $query_var => $args ) {
+ set_query_var( $query_var, $args );
+ }
+
+ // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- $this->template is validated and defined within the class
+ include_once $this->template;
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php b/plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php
new file mode 100644
index 00000000..2fc32bd2
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php
@@ -0,0 +1,155 @@
+get_settings_group();
+ $option_name = $this->get_option_key();
+
+ register_setting(
+ $option_group,
+ $option_name,
+ [
+ 'sanitize_callback' => [ $this, 'sanitize_settings' ],
+ 'type' => 'array',
+ 'default' => [],
+ ]
+ );
+
+ foreach ( $this->field_collection->get_tabs() as $tab ) {
+ $this->render_tab_section( $tab->get_name(), $tab->get_label() );
+ }
+ }
+
+ /**
+ * Sanitize and merge new settings per-tab, pruning unknown fields.
+ *
+ * @param array|null $new_input New settings input for the specific tab that comes from the form for the sanitization.
+ *
+ * @return array
+ */
+ public function sanitize_settings( ?array $new_input ): array {
+ if ( is_null( $new_input ) ) {
+ return [];
+ }
+
+ $option_name = $this->get_option_key();
+
+ $old_input = (array) get_option( $option_name, [] );
+
+ // Remove redundant tabs.
+ $tabs = $this->field_collection->get_tabs();
+ $tab_keys = array_keys( $tabs );
+
+ if ( empty( $tab_keys ) ) {
+ return $old_input;
+ }
+
+ $old_input = array_intersect_key( $old_input, array_flip( $tab_keys ) );
+
+ $tab = array_keys( $new_input );
+ if ( ! isset( $tab[0] ) ) {
+ return $old_input;
+ }
+
+ $tab_to_sanitize = (string) $tab[0];
+ if ( ! is_array( $new_input[ $tab_to_sanitize ] ) ) {
+ return $old_input;
+ }
+
+ $sanitized_fields = [];
+ foreach ( $new_input[ $tab_to_sanitize ] as $key => $value ) {
+ $field = $this->field_collection->get_field( $key );
+
+ // Skip unknown fields.
+ if ( is_null( $field ) ) {
+ continue;
+ }
+
+ $sanitized_fields[ $key ] = $field->sanitize_field( $value );
+ }
+
+ // Merge the sanitized fields with the old input.
+ $old_input[ $tab_to_sanitize ] = $sanitized_fields;
+
+ return $old_input;
+ }
+
+ /**
+ * Get the option key for the settings group.
+ */
+ public function get_option_key(): string {
+ return Logging_Settings_Service::get_option_key();
+ }
+
+ /**
+ * Get the settings group for the options.
+ */
+ public function get_settings_group(): string {
+ return Logging_Settings_Service::get_settings_group();
+ }
+
+ /**
+ * Render the settings section for a specific tab.
+ *
+ * This method creates a settings section for the given tab and renders the fields for that section.
+ *
+ * @param string $tab_key The tab key.
+ * @param string $label The label for the tab section.
+ */
+ protected function render_tab_section( string $tab_key, string $label ): void {
+ $fields = $this->field_collection->get_fields();
+ $page_id = 'wpgraphql_logging_section_' . $tab_key;
+ $page_uri = 'wpgraphql-logging-' . $tab_key;
+
+ add_settings_section( $page_id, $label, static fn() => null, $page_uri );
+
+ /** @var \WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface $field */
+ foreach ( $fields as $field ) {
+ if ( ! $field->should_render_for_tab( $tab_key ) ) {
+ continue;
+ }
+
+ $field->add_settings_field(
+ $page_id,
+ $page_uri,
+ [
+ 'tab_key' => $tab_key,
+ 'label' => $label,
+ 'settings_key' => $this->get_option_key(),
+ ]
+ );
+ }
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php
new file mode 100644
index 00000000..02388e6b
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php
@@ -0,0 +1,121 @@
+
+
+
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings_Page.php b/plugins/wpgraphql-logging/src/Admin/Settings_Page.php
new file mode 100644
index 00000000..f49650a8
--- /dev/null
+++ b/plugins/wpgraphql-logging/src/Admin/Settings_Page.php
@@ -0,0 +1,212 @@
+setup();
+ }
+
+ /**
+ * Fire off init action.
+ *
+ * @param \WPGraphQL\Logging\Admin\Settings_Page $instance the instance of the plugin class.
+ */
+ do_action( 'wpgraphql_logging_settings_init', self::$instance );
+
+ return self::$instance;
+ }
+
+ /**
+ * Sets up the settings page by registering hooks.
+ */
+ public function setup(): void {
+ add_action( 'init', [ $this, 'init_field_collection' ], 10, 0 );
+ add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0 );
+ add_action( 'admin_init', [ $this, 'register_settings_fields' ], 10, 0 );
+ add_action( 'admin_enqueue_scripts', [ $this, 'load_scripts_styles' ], 10, 1 );
+ }
+
+ /**
+ * Initialize the field collection.
+ */
+ public function init_field_collection(): void {
+ $this->field_collection = new Settings_Field_Collection();
+ }
+
+ /**
+ * Registers the settings page.
+ */
+ public function register_settings_page(): void {
+ if ( is_null( $this->field_collection ) ) {
+ return;
+ }
+
+ $tabs = $this->field_collection->get_tabs();
+
+ $tab_labels = [];
+ foreach ( $tabs as $tab_key => $tab ) {
+ if ( ! is_a( $tab, Settings_Tab_Interface::class ) ) {
+ continue;
+ }
+
+ $tab_labels[ $tab_key ] = $tab->get_label();
+ }
+
+ $page = new Menu_Page(
+ __( 'WPGraphQL Logging Settings', 'wpgraphql-logging' ),
+ 'WPGraphQL Logging',
+ self::PLUGIN_MENU_SLUG,
+ trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'src/Admin/Settings/Templates/admin.php',
+ [
+ 'wpgraphql_logging_main_page_config' => [
+ 'tabs' => $tab_labels,
+ 'current_tab' => $this->get_current_tab(),
+ ],
+ ],
+ );
+
+ $page->register_page();
+ }
+
+ /**
+ * Registers the settings fields for each tab.
+ */
+ public function register_settings_fields(): void {
+ if ( ! isset( $this->field_collection ) ) {
+ return;
+ }
+ $settings_manager = new Settings_Form_Manager( $this->field_collection );
+ $settings_manager->render_form();
+ }
+
+ /**
+ * Get the current tab for the settings page.
+ *
+ * @param array $tabs Optional. The available tabs. If not provided, uses the instance tabs.
+ *
+ * @return string The current tab slug.
+ */
+ public function get_current_tab( array $tabs = [] ): string {
+ $tabs = $this->get_tabs( $tabs );
+ if ( empty( $tabs ) ) {
+ return 'basic_configuration';
+ }
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for tab navigation only, no form processing
+ if ( ! isset( $_GET['tab'] ) || ! is_string( $_GET['tab'] ) ) {
+ return 'basic_configuration';
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for tab navigation only, no form processing
+ $tab = sanitize_text_field( $_GET['tab'] );
+
+ if ( ! is_string( $tab ) || '' === $tab ) {
+ return 'basic_configuration';
+ }
+
+ if ( array_key_exists( $tab, $tabs ) ) {
+ return $tab;
+ }
+
+ return 'basic_configuration';
+ }
+
+ /**
+ * Load scripts and styles for the admin page.
+ *
+ * @param string $hook_suffix The current admin page hook suffix.
+ */
+ public function load_scripts_styles( string $hook_suffix ): void {
+ // Only load on our settings page.
+ if ( ! str_contains( $hook_suffix, self::PLUGIN_MENU_SLUG ) ) {
+ return;
+ }
+
+ // Enqueue admin styles if they exist.
+ $style_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/css/settings/wp-graphql-logging-settings.css';
+ if ( file_exists( trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'assets/css/settings/wp-graphql-logging-settings.css' ) ) {
+ wp_enqueue_style(
+ 'wpgraphql-logging-settings-css',
+ $style_path,
+ [],
+ WPGRAPHQL_LOGGING_VERSION
+ );
+ }
+
+ // Enqueue admin scripts if they exist.
+ $script_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/js/settings/wp-graphql-logging-settings.js';
+ if ( ! file_exists( trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'assets/js/settings/wp-graphql-logging-settings.js' ) ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'wpgraphql-logging-settings-js',
+ $script_path,
+ [],
+ WPGRAPHQL_LOGGING_VERSION,
+ true
+ );
+ }
+
+ /**
+ * Get the tabs for the settings page.
+ *
+ * @param array $tabs Optional. The available tabs. If not provided, uses the instance tabs.
+ *
+ * @return array The tabs.
+ */
+ protected function get_tabs(array $tabs = []): array {
+ if ( ! empty( $tabs ) ) {
+ return $tabs;
+ }
+ if ( ! is_null( $this->field_collection ) ) {
+ return $this->field_collection->get_tabs();
+ }
+
+ return [];
+ }
+}
diff --git a/plugins/wpgraphql-logging/src/Admin/View/.gitkeep b/plugins/wpgraphql-logging/src/Admin/View/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/wpgraphql-logging/src/Events/Events.php b/plugins/wpgraphql-logging/src/Events/Events.php
index 4049ed9b..6a2094f8 100644
--- a/plugins/wpgraphql-logging/src/Events/Events.php
+++ b/plugins/wpgraphql-logging/src/Events/Events.php
@@ -32,11 +32,4 @@ final class Events {
* @var string
*/
public const BEFORE_RESPONSE_RETURNED = 'graphql_return_response';
-
- /**
- * After the request is processed.
- *
- * @var string
- */
- public const POST_REQUEST = 'post_request';
}
diff --git a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php
index 5104b7dc..32c216f8 100644
--- a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php
+++ b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php
@@ -9,6 +9,7 @@
use WPGraphQL\Logging\Logger\LoggerService;
use WPGraphQL\Request;
use WPGraphQL\WPSchema;
+use WPGraphQL\Logging\Admin\Settings\Fields\Tab\Basic_Configuration_Tab;
/**
* WPGraphQL Query Event Lifecycle.
@@ -39,6 +40,8 @@ class QueryEventLifecycle {
*/
protected function __construct( LoggerService $logger ) {
$this->logger = $logger;
+ $full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] );
+ $this->config = $full_config['basic_configuration'] ?? [];
}
/**
@@ -54,6 +57,40 @@ public static function init(): QueryEventLifecycle {
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;
+ }
+ }
+
+ // 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;
+ }
+
+ return true;
+ }
+
/**
* Initial Incoming Request.
*
@@ -65,6 +102,10 @@ public static function init(): QueryEventLifecycle {
*/
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,
@@ -102,6 +143,10 @@ public function log_pre_request( string $query, ?string $operation_name, ?array
*/
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 = [
@@ -133,8 +178,6 @@ public function log_graphql_before_execute(Request $request ): void {
}
}
-
-
/**
* Before the GraphQL response is returned to the client.
*
@@ -151,6 +194,9 @@ public function log_graphql_before_execute(Request $request ): void {
*/
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,
diff --git a/plugins/wpgraphql-logging/src/Plugin.php b/plugins/wpgraphql-logging/src/Plugin.php
index de3d7142..8c9ad882 100644
--- a/plugins/wpgraphql-logging/src/Plugin.php
+++ b/plugins/wpgraphql-logging/src/Plugin.php
@@ -4,6 +4,7 @@
namespace WPGraphQL\Logging;
+use WPGraphQL\Logging\Admin\Settings_Page;
use WPGraphQL\Logging\Events\EventManager;
use WPGraphQL\Logging\Events\QueryEventLifecycle;
use WPGraphQL\Logging\Logger\Database\DatabaseEntity;
@@ -52,6 +53,7 @@ public static function init(): self {
* Initialize the plugin admin, frontend & api functionality.
*/
public function setup(): void {
+ Settings_Page::init();
QueryEventLifecycle::init();
}
diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php
new file mode 100644
index 00000000..1ff8290f
--- /dev/null
+++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php
@@ -0,0 +1,69 @@
+field = new Checkbox_Field(
+ 'enable_logging',
+ 'basic_configuration',
+ 'Enable Logging',
+ 'custom-css-class',
+ 'Enable or disable query logging for WPGraphQL requests.'
+ );
+ }
+
+ public function test_basic_field_properties(): void {
+ $field = $this->field;
+ $this->assertEquals( 'enable_logging', $field->get_id() );
+ $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) );
+ $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) );
+ }
+
+ public function test_sanitize_field(): void {
+ $field = $this->field;
+ $this->assertTrue( $field->sanitize_field( true ) );
+ $this->assertTrue( $field->sanitize_field( 1 ) );
+ $this->assertFalse( $field->sanitize_field( false ) );
+ $this->assertFalse( $field->sanitize_field( 0 ) );
+ $this->assertFalse( $field->sanitize_field( null ) );
+ }
+
+ public function test_render_field(): void {
+ $field = $this->field;
+ $option_value = [];
+ $setting_key = 'wpgraphql_logging_settings';
+ $tab_key = 'basic_configuration';
+ $args = [
+ 'tab_key' => $tab_key,
+ 'settings_key' => $setting_key,
+ ];
+
+ ob_start();
+ $field->render_field_callback( $args );
+ $rendered_output = ob_get_contents();
+ ob_end_clean();
+
+ $expected_output = <<
+
+ Enable or disable query logging for WPGraphQL requests.
+
+HTML;
+ $this->assertEquals(
+ preg_replace('/[\s\t\r\n]+/', '', $expected_output),
+ preg_replace('/[\s\t\r\n]+/', '', $rendered_output)
+ );
+
+ }
+}
diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php
new file mode 100644
index 00000000..eef473b8
--- /dev/null
+++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php
@@ -0,0 +1,109 @@
+field = new Select_Field(
+ 'log_level',
+ 'basic_configuration',
+ 'Log Level',
+ [
+ 'debug' => 'Debug',
+ 'info' => 'Info',
+ 'warning' => 'Warning',
+ 'error' => 'Error',
+ ],
+ 'custom-css-class',
+ 'Select the minimum log level for WPGraphQL queries.',
+ false
+ );
+
+ $this->multipleField = new Select_Field(
+ 'query_types',
+ 'basic_configuration',
+ 'Query Types',
+ [
+ 'query' => 'Query',
+ 'mutation' => 'Mutation',
+ 'subscription' => 'Subscription',
+ ],
+ 'multiple-select-class',
+ 'Select which query types to log.',
+ true
+ );
+ }
+
+ public function test_basic_field_properties(): void {
+ $field = $this->field;
+ $this->assertEquals( 'log_level', $field->get_id() );
+ $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) );
+ $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) );
+ }
+
+ public function test_multiple_field_properties(): void {
+ $field = $this->multipleField;
+ $this->assertEquals( 'query_types', $field->get_id() );
+ }
+
+ public function test_sanitize_field_single_select(): void {
+ $field = $this->field;
+
+ $this->assertEquals( 'debug', $field->sanitize_field( 'debug' ) );
+ $this->assertEquals( 'info', $field->sanitize_field( 'info' ) );
+ $this->assertEquals( 'warning', $field->sanitize_field( 'warning' ) );
+ $this->assertEquals( 'error', $field->sanitize_field( 'error' ) );
+ $this->assertEquals( '', $field->sanitize_field( 'invalid' ) );
+ $this->assertEquals( '', $field->sanitize_field( 'critical' ) );
+ $this->assertEquals( '', $field->sanitize_field( '' ) );
+ $this->assertEquals( '', $field->sanitize_field( '' ) );
+ $this->assertEquals( 'debug', $field->sanitize_field( 'debug' ) );
+ $this->assertEquals( '', $field->sanitize_field( 123 ) );
+ $this->assertEquals( '', $field->sanitize_field( true ) );
+ $this->assertEquals( '', $field->sanitize_field( false ) );
+ }
+
+ public function test_sanitize_field_multiple_select(): void {
+ $field = $this->multipleField;
+
+ $this->assertEquals( ['query'], $field->sanitize_field( ['query'] ) );
+ $this->assertEquals( ['query', 'mutation'], $field->sanitize_field( ['query', 'mutation'] ) );
+ $this->assertEquals( ['query', 'mutation', 'subscription'], $field->sanitize_field( ['query', 'mutation', 'subscription'] ) );
+ $this->assertEquals( [], $field->sanitize_field( ['invalid', 'another_invalid'] ) );
+ $this->assertEquals( ['query'], $field->sanitize_field( 'query' ) );
+ $this->assertEquals( [], $field->sanitize_field( 'invalid' ) );
+ }
+
+ public function test_render_field_callback(): void {
+ $field = $this->field;
+ $option_value = [];
+ $setting_key = 'wpgraphql_logging_settings';
+ $tab_key = 'basic_configuration';
+ $args = [
+ 'tab_key' => $tab_key,
+ 'settings_key' => $setting_key,
+ ];
+
+ // Capture the echoed output using output buffering
+ ob_start();
+ $field->render_field_callback( $args );
+ $rendered_output = ob_get_contents();
+ ob_end_clean();
+
+ $this->assertStringContainsString( 'name="wpgraphql_logging_settings[basic_configuration][log_level]"', $rendered_output );
+ $this->assertStringContainsString( 'id="log_level"', $rendered_output );
+ $this->assertStringContainsString( 'class="custom-css-class"', $rendered_output );
+ $this->assertStringContainsString( 'Select the minimum log level for WPGraphQL queries.', $rendered_output );
+ }
+}
diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php
new file mode 100644
index 00000000..d2c877dc
--- /dev/null
+++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php
@@ -0,0 +1,190 @@
+field = new Text_Input_Field(
+ 'ip_restrictions',
+ 'basic_configuration',
+ 'IP Restrictions',
+ 'custom-css-class',
+ 'A comma separated list of IP addresses to restrict logging to. Leave empty to log from all IPs.',
+ 'e.g. 192.168.1.1, 10.0.0.1',
+ ''
+ );
+ }
+
+ public function test_basic_field_properties(): void {
+ $field = $this->field;
+ $this->assertEquals( 'ip_restrictions', $field->get_id() );
+ $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) );
+ $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) );
+ $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) );
+ }
+
+
+ public function test_sanitize_field() {
+ $field = $this->field;
+
+ // Valid Input
+ $input = '192.168.1.1, 10.0.0.1';
+ $sanitized = $field->sanitize_field( $input );
+ $this->assertEquals( $input, $sanitized );
+
+ // XSS
+ $input = '192.168.1.1, 10.0.0.1';
+ $sanitized = $field->sanitize_field( $input );
+ $this->assertStringNotContainsString( '