diff --git a/application/clicommands/Command.php b/application/clicommands/Command.php index 3b01416c..e30494cf 100644 --- a/application/clicommands/Command.php +++ b/application/clicommands/Command.php @@ -21,7 +21,7 @@ class Command extends CliCommand { private $loopStarted = false; - protected $logger; + protected Logger $logger; /** @var RemoteClient */ protected $remoteClient; diff --git a/application/clicommands/DaemonCommand.php b/application/clicommands/DaemonCommand.php index 6cb56cb6..2c079070 100644 --- a/application/clicommands/DaemonCommand.php +++ b/application/clicommands/DaemonCommand.php @@ -2,9 +2,12 @@ namespace Icinga\Module\Vspheredb\Clicommands; +use gipfl\Log\Filter\LogLevelFilter; use gipfl\SimpleDaemon\Daemon; use Icinga\Module\Vspheredb\Daemon\RpcNamespace\RpcNamespaceProcess; use Icinga\Module\Vspheredb\Daemon\VsphereDbDaemon; +use Icinga\Module\Vspheredb\Db; +use Throwable; class DaemonCommand extends Command { @@ -20,6 +23,7 @@ public function runAction() $this->assertRequiredExtensionsAreLoaded(); $this->assertNoVcenterParam(); $daemon = new Daemon(); + $this->eventuallyApplyConfiguredLogLevel(); $daemon->setLogger($this->logger); $vSphereDb = new VsphereDbDaemon(); $vSphereDb->on(RpcNamespaceProcess::ON_RESTART, function () use ($daemon) { @@ -30,6 +34,39 @@ public function runAction() $this->eventuallyStartMainLoop(); } + protected function eventuallyApplyConfiguredLogLevel(): void + { + // Setting the log level via CLI flags has higher precedence than database config + if ($this->isVerbose || $this->isDebugging) { + return; + } + + $db = null; + try { + $db = Db::newConfiguredInstance()->getDbAdapter(); + $level = $db->fetchOne( + $db->select() + ->from('daemon_config', ['value']) + ->where('`key` = ?', 'log_level') + ); + if (! is_string($level) || $level === '') { + return; + } + + $newFilter = new LogLevelFilter($level); + $this->logger->addFilter($newFilter); + foreach ($this->logger->getFilters() as $filter) { + if ($filter instanceof LogLevelFilter && $filter !== $newFilter) { + $this->logger->removeFilter($filter); + } + } + } catch (Throwable) { + // Keep current log level if DB config is not available yet. + } finally { + $db?->closeConnection(); + } + } + protected function assertNoVcenterParam() { if ($this->params->get('vCenterId')) { diff --git a/application/controllers/DaemonController.php b/application/controllers/DaemonController.php index 0566d15f..2b3c731e 100644 --- a/application/controllers/DaemonController.php +++ b/application/controllers/DaemonController.php @@ -42,7 +42,7 @@ public function indexAction() protected function prepareLogSettings() { - $logLevelForm = new LogLevelForm($this->remoteClient(), $this->loop()); + $logLevelForm = new LogLevelForm($this->remoteClient(), $this->loop(), $this->db()->getDbAdapter()); $logLevelForm->on($logLevelForm::ON_SUCCESS, function () { $this->redirectNow($this->url()); }); diff --git a/library/Vspheredb/Daemon/RpcNamespace/RpcNamespaceLogger.php b/library/Vspheredb/Daemon/RpcNamespace/RpcNamespaceLogger.php index a4da4941..79781911 100644 --- a/library/Vspheredb/Daemon/RpcNamespace/RpcNamespaceLogger.php +++ b/library/Vspheredb/Daemon/RpcNamespace/RpcNamespaceLogger.php @@ -40,15 +40,11 @@ public function setLogLevelRequest($level) $this->logger->notice("Will change the log level to '$level'"); } $this->logger->addFilter($newFilter); - $remove = []; foreach ($this->logger->getFilters() as $filter) { if ($filter instanceof LogLevelFilter && $filter !== $newFilter) { - $remove[] = $filter; + $this->logger->removeFilter($filter); } } - foreach ($remove as $filter) { - $this->logger->removeFilter($filter); - } if ($newFilter->wants('notice', '')) { $this->logger->notice("Changed log level to '$level'"); } diff --git a/library/Vspheredb/Daemon/VsphereDbDaemon.php b/library/Vspheredb/Daemon/VsphereDbDaemon.php index ab6c057f..f2edf01b 100644 --- a/library/Vspheredb/Daemon/VsphereDbDaemon.php +++ b/library/Vspheredb/Daemon/VsphereDbDaemon.php @@ -118,7 +118,6 @@ public function start(LoopInterface $loop) $this->loop = $loop; $logger = $this->logger; $this->daemonState = $this->initializeDaemonState(); - $this->setInitialDaemonState(); $this->detectProcessInfo(); $this->initializeDbLogger($logger); // TODO: move to sub process. Hint: needs no DB, has a queue $this->prepareApi($loop, $logger); @@ -152,15 +151,10 @@ protected function initializeDaemonState() } $this->componentStates = $daemonState->getComponentStates(); }); - - return $daemonState; - } - - protected function setInitialDaemonState() - { - $daemonState = $this->daemonState; $daemonState->setProcessTitle(self::PROCESS_NAME); $daemonState->setState(self::STATE_STARTING); + + return $daemonState; } protected function onComponentChange($component, $formerState, $currentState) @@ -174,77 +168,79 @@ protected function onComponentChange($component, $formerState, $currentState) $this->daemonState->getComponentState($component) )); } - if ($component === self::COMPONENT_DB) { - if ($formerState === self::STATE_READY) { - $this->stopConfigWatch(); - $this->stopComponent(self::COMPONENT_API); - $this->stopComponent(self::COMPONENT_LOCALDB); - } elseif ($currentState === self::STATE_IDLE) { - $this->runConfigWatch(); - } elseif ($currentState === self::STATE_READY) { - $this->setLocalDbState(self::STATE_STARTING); - } - if ($currentState === self::STATE_FAILED) { - $this->loop->addTimer(10, function () { - $this->stopDbProcess(); + switch ($component) { + case self::COMPONENT_DB: + if ($formerState === self::STATE_READY) { $this->stopConfigWatch(); - if ($this->daemonState->getComponentState(self::COMPONENT_DB) === self::STATE_FAILED) { - $this->initializeDbProcess(); - } - }); - } - } - if ($component === self::COMPONENT_LOCALDB) { - if ($formerState === self::STATE_READY) { - $this->stopComponent(self::COMPONENT_API); - } - switch ($currentState) { - case self::STATE_STARTING: - $this->reconnectToDb(); - break; - case self::STATE_FAILED: - $this->logger->error('Failed. Will try to reconnect to the Database'); - $this->eventuallyDisconnectFromDb(); - $delay = $this->delayOnFailed; - $this->logger->warning("Failed. Reconnecting in {$delay}s"); - $this->loop->addTimer($delay, function () { - if ($this->getLocalDbState() === self::STATE_FAILED) { - $this->setLocalDbState(self::STATE_STARTING); + $this->stopComponent(self::COMPONENT_API); + $this->stopComponent(self::COMPONENT_LOCALDB); + } elseif ($currentState === self::STATE_IDLE) { + $this->runConfigWatch(); + } elseif ($currentState === self::STATE_READY) { + $this->setLocalDbState(self::STATE_STARTING); + } + if ($currentState === self::STATE_FAILED) { + $this->loop->addTimer(10, function () { + $this->stopDbProcess(); + $this->stopConfigWatch(); + if ($this->daemonState->getComponentState(self::COMPONENT_DB) === self::STATE_FAILED) { + $this->initializeDbProcess(); } }); - break; - case self::STATE_READY: - $this->setApiState(self::STATE_STARTING); - break; - case self::STATE_STOPPING: - $this->eventuallyDisconnectFromDb(); + } + break; + case self::COMPONENT_LOCALDB: + if ($formerState === self::STATE_READY) { $this->stopComponent(self::COMPONENT_API); - $this->setLocalDbState(self::STATE_STOPPED); - break; - } - } - if ($component === self::COMPONENT_API) { - switch ($currentState) { - case self::STATE_STARTING: - $this->apiConnectionHandler->run($this->loop); - $this->setApiState(self::STATE_READY); - break; - case self::STATE_READY: - $this->refreshConfiguredServers(); - $this->daemonState->setState(self::STATE_READY); - break; - case self::STATE_FAILED: - $this->logger->error('[api] failed'); + } + switch ($currentState) { + case self::STATE_STARTING: + $this->reconnectToDb(); + break; + case self::STATE_FAILED: + $this->logger->error('Failed. Will try to reconnect to the Database'); + $this->eventuallyDisconnectFromDb(); + $delay = $this->delayOnFailed; + $this->logger->warning("Failed. Reconnecting in {$delay}s"); + $this->loop->addTimer($delay, function () { + if ($this->getLocalDbState() === self::STATE_FAILED) { + $this->setLocalDbState(self::STATE_STARTING); + } + }); + break; + case self::STATE_READY: + $this->setApiState(self::STATE_STARTING); + break; + case self::STATE_STOPPING: + $this->eventuallyDisconnectFromDb(); + $this->stopComponent(self::COMPONENT_API); + $this->setLocalDbState(self::STATE_STOPPED); + break; + } + break; + case self::COMPONENT_API: + switch ($currentState) { + case self::STATE_STARTING: + $this->apiConnectionHandler->run($this->loop); + $this->setApiState(self::STATE_READY); + break; + case self::STATE_READY: + $this->refreshConfiguredServers(); + $this->daemonState->setState(self::STATE_READY); + break; + case self::STATE_FAILED: + $this->logger->error('[api] failed'); // Intentional fall-through: // no break - case self::STATE_STOPPING: - if ($this->apiConnectionHandler) { - $this->apiConnectionHandler->stop(); - } - $this->stopAllApiTasks(); - $this->setApiState(self::STATE_STOPPED); - break; - } + case self::STATE_STOPPING: + if ($this->apiConnectionHandler) { + $this->apiConnectionHandler->stop(); + } + $this->stopAllApiTasks(); + $this->setApiState(self::STATE_STOPPED); + break; + } + break; } } diff --git a/library/Vspheredb/Web/Form/LogLevelForm.php b/library/Vspheredb/Web/Form/LogLevelForm.php index 2c77ad21..ec2c69c7 100644 --- a/library/Vspheredb/Web/Form/LogLevelForm.php +++ b/library/Vspheredb/Web/Form/LogLevelForm.php @@ -9,6 +9,7 @@ use ipl\I18n\Translation; use Psr\Log\LogLevel; use React\EventLoop\LoopInterface; +use Zend_Db_Adapter_Abstract; use function React\Async\await; @@ -22,13 +23,17 @@ class LogLevelForm extends InlineForm /** @var LoopInterface */ protected $loop; + /** @var Zend_Db_Adapter_Abstract */ + protected Zend_Db_Adapter_Abstract $db; + /** @var boolean */ protected $talkedToSocket; - public function __construct(RemoteClient $client, LoopInterface $loop) + public function __construct(RemoteClient $client, LoopInterface $loop, Zend_Db_Adapter_Abstract $db) { $this->client = $client; $this->loop = $loop; + $this->db = $db; } public function talkedToSocket() @@ -63,7 +68,24 @@ protected function assemble() protected function onSuccess() { - await($this->client->request('logger.setLogLevel', ['level' => $this->getValue('log_level')])); + $logLevel = $this->getValue('log_level'); + await( + $this->client->request('logger.setLogLevel', ['level' => $logLevel]) + ->then(function () use ($logLevel) { + $query = $this->db->select() + ->from('daemon_config', ['key']) + ->where('`key` = ?', 'log_level'); + if ($this->db->query($query)->rowCount()) { + $this->db->update( + 'daemon_config', + ['value' => $logLevel], + $this->db->quoteInto('`key` = ?', 'log_level') + ); + } else { + $this->db->insert('daemon_config', ['`key`' => 'log_level', 'value' => $logLevel]); + } + }) + ); } protected function listLogLevels() diff --git a/schema/mysql-migrations/upgrade_64.sql b/schema/mysql-migrations/upgrade_64.sql new file mode 100644 index 00000000..c2371bcd --- /dev/null +++ b/schema/mysql-migrations/upgrade_64.sql @@ -0,0 +1,8 @@ +CREATE TABLE daemon_config ( + `key` ENUM('log_level') PRIMARY KEY, + value VARCHAR(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin; + +INSERT INTO vspheredb_schema_migration + (schema_version, migration_time) + VALUES (64, NOW()); diff --git a/schema/mysql.sql b/schema/mysql.sql index 2edeb5c6..f5993e7b 100644 --- a/schema/mysql.sql +++ b/schema/mysql.sql @@ -995,6 +995,11 @@ CREATE TABLE tagging_object_tag ( -- vm_id BIGINT(20) UNSIGNED AUTO_INCREMENT NOT NULL, -- ); +CREATE TABLE daemon_config ( + `key` ENUM('log_level') PRIMARY KEY, + value VARCHAR(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin; + INSERT INTO vspheredb_schema_migration (schema_version, migration_time) - VALUES (63, NOW()); + VALUES (64, NOW());