diff --git a/config/schema/elasticsearch_connector.connector.standard.schema.yml b/config/schema/elasticsearch_connector.connector.standard.schema.yml index b6d20ebcf54204a982fdcf70f730702d989f353a..8da83ae62f6a1fd35a7c9c772da329398cceccfc 100644 --- a/config/schema/elasticsearch_connector.connector.standard.schema.yml +++ b/config/schema/elasticsearch_connector.connector.standard.schema.yml @@ -5,3 +5,6 @@ plugin.plugin_configuration.elasticsearch_connector.standard: url: type: string label: 'The URL to the Elasticsearch cluster.' + enable_debug_logging: + type: boolean + label: 'Enable debugging mode: log ElasticSearch network traffic' diff --git a/src/Plugin/ElasticSearch/Connector/StandardConnector.php b/src/Plugin/ElasticSearch/Connector/StandardConnector.php index 2ca805c13886ac29bedecd309b256487d4e40bdd..fc8990b41ed225b96652657487955226cb53786f 100644 --- a/src/Plugin/ElasticSearch/Connector/StandardConnector.php +++ b/src/Plugin/ElasticSearch/Connector/StandardConnector.php @@ -96,12 +96,14 @@ class StandardConnector extends PluginBase implements ElasticSearchConnectorInte */ public function getClient(): Client { // We only support one host. - // @todo Logging should be configurable. - // https://www.drupal.org/project/elasticsearch_connector/issues/3427092 - return ClientBuilder::create() - ->setHosts([$this->configuration['url']]) - ->setLogger($this->logger) - ->build(); + $clientBuilder = ClientBuilder::create() + ->setHosts([$this->configuration['url']]); + + if ($this->configuration['enable_debug_logging']) { + $clientBuilder->setLogger($this->logger); + } + + return $clientBuilder->build(); } /** @@ -110,6 +112,7 @@ class StandardConnector extends PluginBase implements ElasticSearchConnectorInte public function defaultConfiguration(): array { return [ 'url' => '', + 'enable_debug_logging' => FALSE, ]; } @@ -124,6 +127,14 @@ class StandardConnector extends PluginBase implements ElasticSearchConnectorInte '#default_value' => $this->configuration['url'] ?? '', '#required' => TRUE, ]; + + $form['enable_debug_logging'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable debugging mode: log ElasticSearch network traffic'), + '#description' => $this->t("This will write requests, responses, and response-time information to Drupal's log, which may help you diagnose problems with Drupal's connection to ElasticSearch.<p><strong>Warning</strong>: This setting will result in poor performance and may log a user’s personally identifiable information. This setting is only intended for temporary use and should be disabled when you finish debugging. Logs written while this mode is active will remain in the log until you clear them or the logs are rotated.</p>"), + '#default_value' => $this->configuration['enable_debug_logging'] ?? FALSE, + ]; + return $form; } @@ -142,6 +153,7 @@ class StandardConnector extends PluginBase implements ElasticSearchConnectorInte */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { $this->configuration['url'] = trim($form_state->getValue('url'), '/ '); + $this->configuration['enable_debug_logging'] = (bool) $form_state->getValue('enable_debug_logging'); } } diff --git a/tests/modules/elasticsearch_connector_test/config/install/search_api.server.elasticsearch_server.yml b/tests/modules/elasticsearch_connector_test/config/install/search_api.server.elasticsearch_server.yml index 29f681146a411cec11e1f41b69aabef3d6a13069..1b6baffcbd36c15798cd02ff20a2ac3a77b53dbd 100644 --- a/tests/modules/elasticsearch_connector_test/config/install/search_api.server.elasticsearch_server.yml +++ b/tests/modules/elasticsearch_connector_test/config/install/search_api.server.elasticsearch_server.yml @@ -11,6 +11,7 @@ backend_config: connector: standard connector_config: url: 'http://elasticsearch:9200' + enable_debug_logging: false advanced: fuzziness: auto prefix: test_ diff --git a/tests/src/Kernel/DebugLoggingTest.php b/tests/src/Kernel/DebugLoggingTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d9a6adfa0c8e78f9217c1413f08295f261ea493c --- /dev/null +++ b/tests/src/Kernel/DebugLoggingTest.php @@ -0,0 +1,196 @@ +<?php + +namespace Drupal\Tests\elasticsearch_connector\Kernel; + +use Drupal\elasticsearch_connector\Plugin\search_api\backend\ElasticSearchBackend; +use Drupal\KernelTests\KernelTestBase; +use Drupal\search_api\Entity\Server; + +/** + * Test the enable_debug_logging configuration variables. + */ +class DebugLoggingTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'field', + 'search_api', + 'elasticsearch_connector', + 'dblog', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('search_api', ['search_api_item']); + $this->installConfig([ + 'field', + 'search_api', + 'elasticsearch_connector', + 'dblog', + ]); + } + + /** + * Test that logs are written when debug mode is on. + */ + public function testLogsWhenDebugModeOn(): void { + $this->clearLogMessages(); + $server = $this->setEnableDebugLogging(TRUE); + + $this->makeQueryToElasticSearchBackend($server); + + $this->assertLogMessageLike('Request: GET http://%/_health_report'); + $this->assertLogMessageLike('Headers: @"Host":["%"]%'); + $this->assertLogMessageLike('Response (retry 0): 200'); + $this->assertLogMessageLike('Headers: @"x-elastic-product":["Elasticsearch"]%'); + $this->assertLogMessageLike('Response time in % sec'); + } + + /** + * Test that no logs are written when debug mode is off. + */ + public function testNoLogsWhenDebugModeOff(): void { + $this->clearLogMessages(); + $server = $this->setEnableDebugLogging(FALSE); + + $this->makeQueryToElasticSearchBackend($server); + + $this->assertNoLogMessageLike('Request: GET http://%/_health_report'); + $this->assertNoLogMessageLike('Headers: @"Host":["%"]%'); + $this->assertNoLogMessageLike('Response (retry 0): 200'); + $this->assertNoLogMessageLike('Headers: @"x-elastic-product":["Elasticsearch"]%'); + $this->assertNoLogMessageLike('Response time in % sec'); + } + + /** + * Assert that a message was logged. + * + * @param string $messageLike + * Part of a message to check for. + * + * @throws \Exception + * Throws an Exception if something does not work, or the assertion fails. + */ + protected function assertLogMessageLike(string $messageLike): void { + $this->assertNotEquals(0, $this->numberLogMessagesLike($messageLike)); + } + + /** + * Assert that a message was not logged. + * + * @param string $messageLike + * Part of a message to check for. + * + * @throws \Exception + * Throws an Exception if something does not work, or the assertion fails. + */ + protected function assertNoLogMessageLike(string $messageLike): void { + $this->assertEquals(0, $this->numberLogMessagesLike($messageLike)); + } + + /** + * Clear log messages to prepare for a test. + */ + protected function clearLogMessages(): void { + // Should do what \Drupal\dblog\Form\DblogClearLogConfirmForm::submitForm() + // does. + $this->container->get('database')->truncate('watchdog')->execute(); + } + + /** + * Make a query to the ElasticSearch Backend. + * + * @param \Drupal\search_api\Entity\Server $server + * The server to make a query to. + * + * @throws \Drupal\search_api\SearchApiException + * Throws a Search API exception if anything goes wrong. + * @throws \Elastic\Transport\Exception\NoNodeAvailableException + * Throws a No Node Available exception if all the ElasticSearch hosts are + * offline. + * @throws \Elastic\Elasticsearch\Exception\ClientResponseException + * Throws a Client Response exception if the status code of response is 4xx. + * @throws \Elastic\Elasticsearch\Exception\ServerResponseException + * Throws a Server Response exception if the status code of response is 5xx. + * @throws \RuntimeException + * Throws a Run-time exception if the Search API Server does not have an + * ElasticSearchBackend. + */ + protected function makeQueryToElasticSearchBackend(Server $server): void { + // We run a health check here, but it could be anything that causes network + // traffic to go to the backend. Just make sure to update the + // assertLogMessageLike() and assertNoLogMessageLike() lines above if you do + // something else. + $serverBackend = $server->getBackend(); + if (!$serverBackend instanceof ElasticSearchBackend) { + throw new \RuntimeException('Cannot test a Search API server that is not an ElasticSearchBackend.'); + } + $serverBackend->getClient()->healthReport(); + } + + /** + * Query the database to see if there is a log message LIKE the given string. + * + * @param string $messageLike + * The string to match, as in an SQL LIKE query, i.e.: use '%' to match any + * number of characters, and '_' to match a single character. + * + * @return int + * The number of log messages of type 'elasticsearch_connector_client' whose + * 'message' is LIKE the given $messageLike. + * + * @throws \Exception + * Throws an Exception if there is an error retrieving the number of log + * messages that match the given query. + */ + protected function numberLogMessagesLike(string $messageLike): int { + return $this->container->get('database') + ->select('watchdog', 'w') + ->condition('type', 'elasticsearch_connector_client') + ->condition('message', $messageLike, 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + } + + /** + * Set the debug logging option on the test ElasticSearch server. + * + * @param bool $enableDebugLogging + * TRUE if debug logging should be enabled; FALSE if it should be disabled. + * + * @return \Drupal\search_api\Entity\Server + * A test ElasticSearch server connection. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * Throws an Entity Storage exception if the test ElasticSearch server's + * configuration cannot be saved. + */ + protected function setEnableDebugLogging(bool $enableDebugLogging): Server { + // Note an ElasticSearch cluster should should be accessible at the 'url' + // below, during the test. It's currently set to the URL of the test server + // set up in the '.with-elasticsearch' section of '/.gitlab-ci.yml'. + $newServer = Server::create([ + 'id' => $this->randomMachineName(), + 'backend' => 'elasticsearch', + 'backend_config' => [ + 'connector' => 'standard', + 'connector_config' => [ + 'url' => 'http://elasticsearch:9200', + 'enable_debug_logging' => $enableDebugLogging, + ], + ], + ]); + $newServer->save(); + + return $newServer; + } + +}