diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index f877239ce..c9a1279aa 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -2185,4 +2185,28 @@ class RestUtils interface iModuleExtension { public function __construct(); +} + +/** + * KPI logging extensibility point + * + * KPI Logger extension + */ +interface iKPILoggerExtension +{ + /** + * Init the statistics collected + * + * @return void + */ + public function InitStats(); + + /** + * Add a new KPI to the stats + * + * @param \Combodo\iTop\Core\Kpi\KpiLogData $oKPILogData + * + * @return mixed + */ + public function LogOperation($oKPILogData); } \ No newline at end of file diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 891487c97..6ffb976ff 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -3063,7 +3063,7 @@ EOF } $sCancelButtonOnClickScript .= "$('#form_{$this->m_iFormId} button.cancel').on('click', fOnClick{$this->m_iFormId}CancelButton);"; $oPage->add_ready_script($sCancelButtonOnClickScript); - + $iFieldsCount = count($aFieldsMap); $sJsonFieldsMap = json_encode($aFieldsMap); @@ -4493,7 +4493,9 @@ HTML; /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { - $oExtensionInstance->OnDBInsert($this, self::GetCurrentChange()); + $oKPI = new ExecutionKPI(); + $oExtensionInstance->OnDBInsert($this, self::GetCurrentChange()); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert'); } return $res; @@ -4510,13 +4512,16 @@ HTML; protected function DBCloneTracked_Internal($newKey = null) { - $oNewObj = parent::DBCloneTracked_Internal($newKey); + /** @var cmdbAbstractObject $oNewObj */ + $oNewObj = MetaModel::GetObject(get_class($this), parent::DBCloneTracked_Internal($newKey)); // Invoke extensions after insertion (the object must exist, have an id, etc.) /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { + $oKPI = new ExecutionKPI(); $oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange()); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert'); } return $oNewObj; @@ -4544,7 +4549,9 @@ HTML; /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { + $oKPI = new ExecutionKPI(); $oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange()); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBUpdate'); } } catch (Exception $e) @@ -4590,7 +4597,9 @@ HTML; /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { + $oKPI = new ExecutionKPI(); $oExtensionInstance->OnDBDelete($this, self::GetCurrentChange()); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBDelete'); } return parent::DBDeleteTracked_Internal($oDeletionPlan); @@ -4608,7 +4617,10 @@ HTML; /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { - if ($oExtensionInstance->OnIsModified($this)) + $oKPI = new ExecutionKPI(); + $bIsModified = $oExtensionInstance->OnIsModified($this); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnIsModified'); + if ($bIsModified) { return true; } @@ -4662,7 +4674,9 @@ HTML; /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { + $oKPI = new ExecutionKPI(); $aNewIssues = $oExtensionInstance->OnCheckToWrite($this); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToWrite'); if (is_array($aNewIssues) && (count($aNewIssues) > 0)) // Some extensions return null instead of an empty array { $this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues); @@ -4710,7 +4724,9 @@ HTML; /** @var \iApplicationObjectExtension $oExtensionInstance */ foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { + $oKPI = new ExecutionKPI(); $aNewIssues = $oExtensionInstance->OnCheckToDelete($this); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToDelete'); if (is_array($aNewIssues) && count($aNewIssues) > 0) { $this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues); diff --git a/application/startup.inc.php b/application/startup.inc.php index 95a3a59d2..601f2a21d 100644 --- a/application/startup.inc.php +++ b/application/startup.inc.php @@ -98,4 +98,10 @@ else Session::Set('itop_env', ITOP_DEFAULT_ENV); } $sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE; -MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv); +try { + MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv); +} +catch (MySQLException $e) { + IssueLog::Debug($e->getMessage()); + throw new MySQLException('Could not connect to the DB server', []); +} \ No newline at end of file diff --git a/application/utils.inc.php b/application/utils.inc.php index 39f87c6be..5672bda2c 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -20,6 +20,7 @@ use Combodo\iTop\Application\Helper\Session; use Combodo\iTop\Application\UI\Base\iUIBlock; use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock; +use Combodo\iTop\Service\Module\ModuleService; use ScssPhp\ScssPhp\Compiler; use ScssPhp\ScssPhp\OutputStyle; use ScssPhp\ScssPhp\ValueConverter; @@ -2131,24 +2132,7 @@ class utils */ public static function GetCurrentModuleName($iCallDepth = 0) { - $sCurrentModuleName = ''; - $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); - - foreach(GetModulesInfo() as $sModuleName => $aInfo) - { - if ($aInfo['root_dir'] !== '') - { - $sRootDir = realpath(APPROOT.$aInfo['root_dir']); - - if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) - { - $sCurrentModuleName = $sModuleName; - break; - } - } - } - return $sCurrentModuleName; + return ModuleService::GetInstance()->GetCurrentModuleName($iCallDepth = 0); } /** @@ -2170,24 +2154,7 @@ class utils */ public static function GetCurrentModuleDir($iCallDepth) { - $sCurrentModuleDir = ''; - $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); - - foreach(GetModulesInfo() as $sModuleName => $aInfo) - { - if ($aInfo['root_dir'] !== '') - { - $sRootDir = realpath(APPROOT.$aInfo['root_dir']); - - if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) - { - $sCurrentModuleDir = basename($sRootDir); - break; - } - } - } - return $sCurrentModuleDir; + return ModuleService::GetInstance()->GetCurrentModuleDir($iCallDepth); } /** @@ -2202,12 +2169,7 @@ class utils */ public static function GetCurrentModuleUrl() { - $sDir = static::GetCurrentModuleDir(1); - if ( $sDir !== '') - { - return static::GetAbsoluteUrlModulesRoot().'/'.$sDir; - } - return ''; + return ModuleService::GetInstance()->GetCurrentModuleUrl(); } /** @@ -2217,8 +2179,7 @@ class utils */ public static function GetCurrentModuleSetting($sProperty, $defaultvalue = null) { - $sModuleName = static::GetCurrentModuleName(1); - return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue); + return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue); } /** @@ -2227,12 +2188,7 @@ class utils */ public static function GetCompiledModuleVersion($sModuleName) { - $aModulesInfo = GetModulesInfo(); - if (array_key_exists($sModuleName, $aModulesInfo)) - { - return $aModulesInfo[$sModuleName]['version']; - } - return null; + return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName); } /** @@ -2756,7 +2712,7 @@ HTML; foreach ($aClassMap as $sPHPClass => $sPHPFile) { $bSkipped = false; - + // Check if our class matches name filter, or is in an excluded path if ($sClassNameFilter !== '' && strpos($sPHPClass, $sClassNameFilter) === false) { $bSkipped = true; @@ -2776,7 +2732,7 @@ HTML; $bSkipped = true; // file not found } } - + if(!$bSkipped){ try { $oRefClass = new ReflectionClass($sPHPClass); diff --git a/bootstrap.inc.php b/bootstrap.inc.php index e9fb69663..bccbcefdc 100644 --- a/bootstrap.inc.php +++ b/bootstrap.inc.php @@ -45,6 +45,7 @@ define('MAINTENANCE_MODE_FILE', APPROOT.'data/.maintenance'); define('READONLY_MODE_FILE', APPROOT.'data/.readonly'); $fItopStarted = microtime(true); +$iItopInitialMemory = memory_get_usage(true); if (!isset($GLOBALS['bBypassAutoload']) || $GLOBALS['bBypassAutoload'] == false) { require_once APPROOT.'/lib/autoload.php'; diff --git a/core/MyHelpers.class.inc.php b/core/MyHelpers.class.inc.php index f5260d987..8d08472ca 100644 --- a/core/MyHelpers.class.inc.php +++ b/core/MyHelpers.class.inc.php @@ -419,6 +419,7 @@ class MyHelpers //} return $sOutput; } + } /** @@ -523,5 +524,3 @@ class Str return (strtolower($sString) == $sString); } } - -?> diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index f3946f3db..b70f17cfe 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -606,8 +606,9 @@ class CMDBSource { self::LogDeadLock($e, true); throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); - } - $oKPI->ComputeStats('Query exec (mySQL)', $sSql); + } finally { + $oKPI->ComputeStats('Query exec (mySQL)', $sSql); + } if ($oResult === false) { $aContext = array('query' => $sSql); diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 6cd2e3ff3..d8db58669 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -584,22 +584,22 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ], - 'email_transport_smtp.allow_self_signed' => array( + 'email_transport_smtp.allow_self_signed' => [ 'type' => 'bool', 'description' => 'Allow self signed peer certificates', 'default' => false, 'value' => false, 'source_of_value' => '', 'show_in_conf_sample' => false, - ), - 'email_transport_smtp.verify_peer' => array( + ], + 'email_transport_smtp.verify_peer' => [ 'type' => 'bool', 'description' => 'Verify peer certificate', 'default' => true, 'value' => true, 'source_of_value' => '', 'show_in_conf_sample' => false, - ), + ], 'email_css' => [ 'type' => 'string', 'description' => 'CSS that will override the standard stylesheet used for the notifications', @@ -989,6 +989,14 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ], + 'log_kpi_report_to_extensions_only' => [ + 'type' => 'bool', + 'description' => 'Report only the KPI logging extensions', + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ], 'max_linkset_output' => [ 'type' => 'integer', 'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.', diff --git a/core/counter.class.inc.php b/core/counter.class.inc.php index 86ecc8d80..c43a2fd13 100644 --- a/core/counter.class.inc.php +++ b/core/counter.class.inc.php @@ -188,8 +188,8 @@ final class ItopCounter if (!$hDBLink) { - throw new Exception("Could not connect to the DB server (host=$sDBHost, user=$sDBUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')'); - } + throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser)); + } return $hDBLink; } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 93d3d019e..2f6694cb7 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2321,7 +2321,7 @@ abstract class DBObject implements iDisplay $oKPI = new ExecutionKPI(); $this->DoCheckToWrite(); - $oKPI->ComputeStats('CheckToWrite', get_class($this)); + $oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite'); if (count($this->m_aCheckIssues) == 0) { $this->m_bCheckStatus = true; @@ -2788,8 +2788,12 @@ abstract class DBObject implements iDisplay $sRootClass = MetaModel::GetRootClass($sClass); // Ensure the update of the values (we are accessing the data directly) + $oKPI = new ExecutionKPI(); $this->DoComputeValues(); + $oKPI->ComputeStatsForExtension($this, 'DoComputeValues'); + $oKPI = new ExecutionKPI(); $this->OnInsert(); + $oKPI->ComputeStatsForExtension($this, 'OnInsert'); // If not automatically computed, then check that the key is given by the caller if (!MetaModel::IsAutoIncrementKey($sRootClass)) @@ -2801,7 +2805,7 @@ abstract class DBObject implements iDisplay } // Ultimate check - ensure DB integrity - list($bRes, $aIssues) = $this->CheckToWrite(); + [$bRes, $aIssues] = $this->CheckToWrite(); if (!$bRes) { throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey())); @@ -2916,7 +2920,9 @@ abstract class DBObject implements iDisplay $this->m_aOrigValues[$sAttCode] = $value; } + $oKPI = new ExecutionKPI(); $this->AfterInsert(); + $oKPI->ComputeStatsForExtension($this, 'AfterInsert'); // Activate any existing trigger $sClass = get_class($this); @@ -3153,8 +3159,11 @@ abstract class DBObject implements iDisplay try { + $oKPI = new ExecutionKPI(); $this->DoComputeValues(); - // Stop watches + $oKPI->ComputeStatsForExtension($this, 'DoComputeValues'); + + // Stop watches $sState = $this->GetState(); if ($sState != '') { @@ -3173,7 +3182,9 @@ abstract class DBObject implements iDisplay } } } - $this->OnUpdate(); + $oKPI = new ExecutionKPI(); + $this->OnUpdate(); + $oKPI->ComputeStatsForExtension($this, 'OnUpdate'); $aChanges = $this->ListChanges(); if (count($aChanges) == 0) @@ -3185,7 +3196,7 @@ abstract class DBObject implements iDisplay } // Ultimate check - ensure DB integrity - list($bRes, $aIssues) = $this->CheckToWrite(); + [$bRes, $aIssues] = $this->CheckToWrite(); if (!$bRes) { throw new CoreCannotSaveObjectException(array( @@ -3369,7 +3380,9 @@ abstract class DBObject implements iDisplay $this->m_aModifiedAtt = array(); try { - $this->AfterUpdate(); + $oKPI = new ExecutionKPI(); + $this->AfterUpdate(); + $oKPI->ComputeStatsForExtension($this, 'AfterUpdate'); // Reload to get the external attributes if ($bNeedReload) { diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index 6e70cbc3b..59b369d6f 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -767,7 +767,10 @@ class DBObjectSet implements iDBObjectSetIterator try { + $oKPI = new ExecutionKPI(); $this->m_oSQLResult = CMDBSource::Query($sSQL); + $sOQL = $this->GetPseudoOQL($this->m_oFilter, $this->GetRealSortOrder(), $this->m_iLimitCount, $this->m_iLimitStart, false); + $oKPI->ComputeStats('OQL Query Exec', $sOQL); } catch (MySQLException $e) { // 1116 = ER_TOO_MANY_TABLES @@ -847,8 +850,11 @@ class DBObjectSet implements iDBObjectSetIterator { if (is_null($this->m_iNumTotalDBRows)) { + $oKPI = new ExecutionKPI(); $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true); $resQuery = CMDBSource::Query($sSQL); + $sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), 0, 0, true); + $oKPI->ComputeStats('OQL Query Exec', $sOQL); if (!$resQuery) return 0; $aRow = CMDBSource::FetchArray($resQuery); @@ -859,6 +865,42 @@ class DBObjectSet implements iDBObjectSetIterator return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ?? } + /** + * @param \DBSearch $oFilter + * @param array $aOrder + * @param int $iLimitCount + * @param int $iLimitStart + * @param bool $bCount + * + * @return string + */ + private function GetPseudoOQL($oFilter, $aOrder, $iLimitCount, $iLimitStart, $bCount) + { + $sOQL = ''; + if ($bCount) { + $sOQL .= 'COUNT '; + } + $sOQL .= $oFilter->ToOQL(); + + if ($iLimitCount > 0) { + $sOQL .= ' LIMIT '; + if ($iLimitStart > 0) { + $sOQL .= "$iLimitStart, "; + } + $sOQL .= "$iLimitCount"; + } + + if (count($aOrder) > 0) { + $sOQL .= ' ORDER BY '; + $aOrderBy = []; + foreach ($aOrder as $sAttCode => $bAsc) { + $aOrderBy[] = $sAttCode.' '.($bAsc ? 'ASC' : 'DESC'); + } + $sOQL .= implode(', ', $aOrderBy); + } + return $sOQL; + } + /** * Check if the count exceeds a given limit * @@ -875,8 +917,11 @@ class DBObjectSet implements iDBObjectSetIterator { if (is_null($this->m_iNumTotalDBRows)) { + $oKPI = new ExecutionKPI(); $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); $resQuery = CMDBSource::Query($sSQL); + $sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true); + $oKPI->ComputeStats('OQL Query Exec', $sOQL); if ($resQuery) { $aRow = CMDBSource::FetchArray($resQuery); @@ -887,7 +932,7 @@ class DBObjectSet implements iDBObjectSetIterator { $iCount = 0; } - } + } else { $iCount = $this->m_iNumTotalDBRows; @@ -912,8 +957,11 @@ class DBObjectSet implements iDBObjectSetIterator { if (is_null($this->m_iNumTotalDBRows)) { + $oKPI = new ExecutionKPI(); $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); $resQuery = CMDBSource::Query($sSQL); + $sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true); + $oKPI->ComputeStats('OQL Query Exec', $sOQL); if ($resQuery) { $aRow = CMDBSource::FetchArray($resQuery); @@ -924,7 +972,7 @@ class DBObjectSet implements iDBObjectSetIterator { $iCount = 0; } - } + } else { $iCount = $this->m_iNumTotalDBRows; diff --git a/core/kpi.class.inc.php b/core/kpi.class.inc.php index 446cc7aa6..a6efd775c 100644 --- a/core/kpi.class.inc.php +++ b/core/kpi.class.inc.php @@ -15,6 +15,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see +use Combodo\iTop\Core\Kpi\KpiLogData; +use Combodo\iTop\Service\Module\ModuleService; /** @@ -30,6 +32,8 @@ class ExecutionKPI static protected $m_bEnabled_Memory = false; static protected $m_bBlameCaller = false; static protected $m_sAllowedUser = '*'; + static protected $m_bReportExtensionsOnly = false; + static protected $m_fSlowQueries = 0; static protected $m_aStats = []; // Recurrent operations static protected $m_aExecData = []; // One shot operations @@ -86,14 +90,39 @@ class ExecutionKPI return false; } + static public function SetReportExtensionsOnly($bReportExtensionsOnly) + { + self::$m_bReportExtensionsOnly = $bReportExtensionsOnly; + } + + static public function SetSlowQueries($fSlowQueries) + { + self::$m_fSlowQueries = $fSlowQueries; + } + static public function GetDescription() { $aFeatures = array(); if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration'; if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage'; - $sFeatures = implode(', ', $aFeatures); + $sFeatures = 'Measures: '.implode(', ', $aFeatures); $sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'"; - return "KPI logging is active for $sFor. Measures: $sFeatures"; + $sSlowQueries = ''; + if (self::$m_fSlowQueries > 0) { + $sSlowQueries = ". Slow Queries: ".self::$m_fSlowQueries."s"; + } + + $aExtensions = []; + /** @var \iKPILoggerExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { + $aExtensions[] = ModuleService::GetInstance()->GetModuleNameFromObject($oExtensionInstance); + } + $sExtensions = ''; + if (count($aExtensions) > 0) { + $sExtensions = '. KPI Extensions: ['.implode(', ', $aExtensions).']'; + } + + return "KPI logging is active for $sFor. $sFeatures$sSlowQueries$sExtensions"; } static public function ReportStats() @@ -101,7 +130,28 @@ class ExecutionKPI if (!self::IsEnabled()) return; global $fItopStarted; + global $iItopInitialMemory; $sExecId = microtime(); // id to differentiate the hrefs! + $sRequest = $_SERVER['REQUEST_URI'].' ('.$_SERVER['REQUEST_METHOD'].')'; + if (isset($_POST['operation'])) { + $sRequest .= ' operation: '.$_POST['operation']; + } + + $fStop = MyHelpers::getmicrotime(); + if (($fStop - $fItopStarted) > self::$m_fSlowQueries) { + // Invoke extensions to log the KPI operation + /** @var \iKPILoggerExtension $oExtensionInstance */ + $iCurrentMemory = self::memory_get_usage(); + $iPeakMemory = self::memory_get_peak_usage(); + foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { + $oKPILogData = new KpiLogData(KpiLogData::TYPE_REQUEST, 'Page', $sRequest, $fItopStarted, $fStop, '', $iItopInitialMemory, $iCurrentMemory, $iPeakMemory); + $oExtensionInstance->LogOperation($oKPILogData); + } + } + + if (self::$m_bReportExtensionsOnly) { + return; + } $aBeginTimes = array(); foreach (self::$m_aExecData as $aOpStats) @@ -114,9 +164,9 @@ class ExecutionKPI $sHtml = "
"; $sHtml .= "
"; - $sHtml .= "

KPIs - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")

"; + $sHtml .= "

KPIs - $sRequest

"; $oStarted = DateTime::createFromFormat('U.u', $fItopStarted); - $sHtml .= "

".$oStarted->format('Y-m-d H:i:s.u')."

"; + $sHtml .= '

'.$oStarted->format('Y-m-d H:i:s.u').'

'; $sHtml .= "

log_kpi_user_id: ".UserRights::GetUserId()."

"; $sHtml .= "
"; $sHtml .= ""; @@ -257,7 +307,7 @@ class ExecutionKPI $sTotalInter = round($fTotalInter, 3); $sMinInter = round($fMinInter, 3); $sMaxInter = round($fMaxInter, 3); - if (($fTotalInter >= $fSlowQueries)) + if (($fTotalInter >= self::$m_fSlowQueries)) { if ($bDisplayHeader) { @@ -285,37 +335,19 @@ class ExecutionKPI self::Report($sHtml); } + public static function InitStats() + { + // Invoke extensions to initialize the KPI statistics + /** @var \iKPILoggerExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { + $oExtensionInstance->InitStats(); + } + } public function __construct() { $this->ResetCounters(); - self::Push($this); - } - - /** - * Stack executions to remove children duration from stats - * - * @param \ExecutionKPI $oExecutionKPI - */ - private static function Push(ExecutionKPI $oExecutionKPI) - { - array_push(self::$m_aExecutionStack, $oExecutionKPI); - } - - /** - * Pop current child and count its duration in its parent - * - * @param float|int $fChildDuration - */ - private static function Pop(float $fChildDuration = 0) - { - array_pop(self::$m_aExecutionStack); - // Update the parent's children duration - $oPrevExecutionKPI = end(self::$m_aExecutionStack); - if ($oPrevExecutionKPI) { - $oPrevExecutionKPI->m_fChildrenDuration += $fChildDuration; - } - } + } // Get the duration since startup, and reset the counter for the next measure // @@ -325,7 +357,9 @@ class ExecutionKPI $aNewEntry = null; - if (self::$m_bEnabled_Duration) { + $fStarted = $this->m_fStarted; + $fStopped = $this->m_fStarted; + if (self::$m_bEnabled_Duration) { $fStopped = MyHelpers::getmicrotime(); $aNewEntry = array( 'op' => $sOperationDesc, @@ -336,6 +370,9 @@ class ExecutionKPI $this->m_fStarted = $fStopped; } + $iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory; + $iCurrentMemory = 0; + $iPeakMemory = 0; if (self::$m_bEnabled_Memory) { $iCurrentMemory = self::memory_get_usage(); @@ -345,40 +382,95 @@ class ExecutionKPI } $aNewEntry['mem_begin'] = $this->m_iInitialMemory; $aNewEntry['mem_end'] = $iCurrentMemory; - if (function_exists('memory_get_peak_usage')) - { - $aNewEntry['mem_peak'] = memory_get_peak_usage(); - } + $iPeakMemory = self::memory_get_peak_usage(); + $aNewEntry['mem_peak'] = $iPeakMemory; // Reset for the next operation (if the object is recycled) $this->m_iInitialMemory = $iCurrentMemory; } - if (!is_null($aNewEntry)) + if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) { + // Invoke extensions to log the KPI operation + /** @var \iKPILoggerExtension $oExtensionInstance */ + foreach(MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) + { + $sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1); + $oKPILogData = new KpiLogData( + KpiLogData::TYPE_REPORT, + 'Step', + $sOperationDesc, + $fStarted, + $fStopped, + $sExtension, + $iInitialMemory, + $iCurrentMemory, + $iPeakMemory); + $oExtensionInstance->LogOperation($oKPILogData); + } + } + + if (!is_null($aNewEntry) && !self::$m_bReportExtensionsOnly) { self::$m_aExecData[] = $aNewEntry; } $this->ResetCounters(); } + public function ComputeStatsForExtension($object, $sMethod) + { + $sSignature = ModuleService::GetInstance()->GetModuleMethodSignature($object, $sMethod); + if (utils::StartsWith($sSignature, '[')) { + $this->ComputeStats('Extension', $sSignature); + } + } + public function ComputeStats($sOperation, $sArguments) { $fDuration = 0; if (self::$m_bEnabled_Duration) { $fStopped = MyHelpers::getmicrotime(); $fDuration = $fStopped - $this->m_fStarted; - $fSelfDuration = $fDuration - $this->m_fChildrenDuration; - if (self::$m_bBlameCaller) { - self::$m_aStats[$sOperation][$sArguments][] = array( - 'time' => $fSelfDuration, - 'callers' => MyHelpers::get_callstack(1), - ); - } else { - self::$m_aStats[$sOperation][$sArguments][] = array( - 'time' => $fSelfDuration, - ); - } - } - self::Pop($fDuration); + $aCallstack = []; + if (!self::$m_bReportExtensionsOnly) { + if (self::$m_bBlameCaller) { + $aCallstack = MyHelpers::get_callstack(1); + self::$m_aStats[$sOperation][$sArguments][] = [ + 'time' => $fDuration, + 'callers' => $aCallstack, + ]; + } else { + self::$m_aStats[$sOperation][$sArguments][] = [ + 'time' => $fDuration + ]; + } + } + + $iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory; + $iCurrentMemory = 0; + $iPeakMemory = 0; + if (self::$m_bEnabled_Memory) + { + $iCurrentMemory = self::memory_get_usage(); + $iPeakMemory = self::memory_get_peak_usage(); + } + + // Invoke extensions to log the KPI operation + /** @var \iKPILoggerExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { + $sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1); + $oKPILogData = new KpiLogData( + KpiLogData::TYPE_STATS, + $sOperation, + $sArguments, + $this->m_fStarted, + $fStopped, + $sExtension, + $iInitialMemory, + $iCurrentMemory, + $iPeakMemory, + $aCallstack); + $oExtensionInstance->LogOperation($oKPILogData); + } + } } protected function ResetCounters() @@ -408,35 +500,7 @@ class ExecutionKPI static protected function memory_get_usage() { - if (function_exists('memory_get_usage')) - { - return memory_get_usage(true); - } - - // Copied from the PHP manual - // - //If its Windows - //Tested on Win XP Pro SP2. Should work on Win 2003 Server too - //Doesn't work for 2000 - //If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642 - if (substr(PHP_OS,0,3) == 'WIN') - { - $output = array(); - exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output); - - return preg_replace( '/[\D]/', '', $output[5] ) * 1024; - } - else - { - //We now assume the OS is UNIX - //Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4 - //This should work on most UNIX systems - $pid = getmypid(); - exec("ps -eo%mem,rss,pid | grep $pid", $output); - $output = explode(" ", $output[0]); - //rss is given in 1024 byte units - return $output[1] * 1024; - } + return memory_get_usage(true); } static public function memory_get_peak_usage($bRealUsage = false) diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 7acdabafc..a185a3510 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -3001,32 +3001,7 @@ abstract class MetaModel // Build the list of available extensions // - $aInterfaces = [ - 'iApplicationUIExtension', - 'iPreferencesExtension', - 'iApplicationObjectExtension', - 'iLoginFSMExtension', - 'iLoginUIExtension', - 'iLogoutExtension', - 'iQueryModifier', - 'iOnClassInitialization', - 'iPopupMenuExtension', - 'iPageUIExtension', - 'iPageUIBlockExtension', - 'iBackofficeLinkedScriptsExtension', - 'iBackofficeEarlyScriptExtension', - 'iBackofficeScriptExtension', - 'iBackofficeInitScriptExtension', - 'iBackofficeReadyScriptExtension', - 'iBackofficeLinkedStylesheetsExtension', - 'iBackofficeStyleExtension', - 'iBackofficeDictEntriesExtension', - 'iBackofficeDictEntriesPrefixesExtension', - 'iPortalUIExtension', - 'ModuleHandlerApiInterface', - 'iNewsroomProvider', - 'iModuleExtension', - ]; + $aInterfaces = array('iApplicationUIExtension', 'iPreferencesExtension', 'iApplicationObjectExtension', 'iLoginFSMExtension', 'iLoginUIExtension', 'iLogoutExtension', 'iQueryModifier', 'iOnClassInitialization', 'iPopupMenuExtension', 'iPageUIExtension', 'iPortalUIExtension', 'ModuleHandlerApiInterface', 'iNewsroomProvider', 'iModuleExtension', 'iKPILoggerExtension'); foreach($aInterfaces as $sInterface) { self::$m_aExtensionClassNames[$sInterface] = array(); @@ -6588,7 +6563,9 @@ abstract class MetaModel ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration')); ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory')); - ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id')); + ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id')); + ExecutionKPI::SetReportExtensionsOnly(self::$m_oConfig->Get('log_kpi_report_to_extensions_only')); + ExecutionKPI::SetSlowQueries(self::$m_oConfig->Get('log_kpi_slow_queries')); self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write'); self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys'); @@ -6713,6 +6690,7 @@ abstract class MetaModel CMDBSource::InitFromConfig(self::$m_oConfig); // Later when timezone implementation is correctly done: CMDBSource::SetTimezone($sDBTimezone); + ExecutionKPI::InitStats(); } /** diff --git a/core/mutex.class.inc.php b/core/mutex.class.inc.php index b94f31bde..db7a4f387 100644 --- a/core/mutex.class.inc.php +++ b/core/mutex.class.inc.php @@ -257,7 +257,7 @@ class iTopMutex $this->hDBLink = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, false); if (!$this->hDBLink) { - throw new Exception("Could not connect to the DB server (host=$sServer, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')'); + throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser)); } // Make sure that the server variable `wait_timeout` is at least 86400 seconds for this connection, diff --git a/core/trigger.class.inc.php b/core/trigger.class.inc.php index 303aacf8d..ef2bb871f 100644 --- a/core/trigger.class.inc.php +++ b/core/trigger.class.inc.php @@ -117,7 +117,9 @@ abstract class Trigger extends cmdbAbstractObject $oAction = MetaModel::GetObject('Action', $iActionId); if ($oAction->IsActive()) { + $oKPI = new ExecutionKPI(); $oAction->DoExecute($this, $aContextArgs); + $oKPI->ComputeStatsForExtension($oAction, 'DoExecute'); } } } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 0dc29f123..acd509563 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -310,6 +310,7 @@ return array( 'Combodo\\iTop\\Core\\DbConnectionWrapper' => $baseDir . '/core/DbConnectionWrapper.php', 'Combodo\\iTop\\Core\\Email\\EmailFactory' => $baseDir . '/sources/Core/Email/EmailFactory.php', 'Combodo\\iTop\\Core\\Email\\iEMail' => $baseDir . '/sources/Core/Email/iEMail.php', + 'Combodo\\iTop\\Core\\Kpi\\KpiLogData' => $baseDir . '/sources/Core/Kpi/KpiLogData.php', 'Combodo\\iTop\\Core\\MetaModel\\FriendlyNameType' => $baseDir . '/sources/Core/MetaModel/FriendlyNameType.php', 'Combodo\\iTop\\Core\\MetaModel\\HierarchicalKey' => $baseDir . '/sources/Core/MetaModel/HierarchicalKey.php', 'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php', @@ -364,6 +365,7 @@ return array( 'Combodo\\iTop\\Renderer\\FieldRenderer' => $baseDir . '/sources/Renderer/FieldRenderer.php', 'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php', + 'Combodo\\iTop\\Service\\Module\\ModuleService' => $baseDir . '/sources/Service/Module/ModuleService.php', 'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', @@ -2944,6 +2946,7 @@ return array( 'iDBObjectSetIterator' => $baseDir . '/core/dbobjectiterator.php', 'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php', 'iDisplay' => $baseDir . '/core/dbobject.class.php', + 'iKPILoggerExtension' => $baseDir . '/application/applicationextension.inc.php', 'iKeyboardShortcut' => $baseDir . '/sources/application/UI/Hook/iKeyboardShortcut.php', 'iLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php', 'iLoginExtension' => $baseDir . '/application/applicationextension.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 026b5cc7c..6fc0575bf 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -678,6 +678,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239 'Combodo\\iTop\\Core\\DbConnectionWrapper' => __DIR__ . '/../..' . '/core/DbConnectionWrapper.php', 'Combodo\\iTop\\Core\\Email\\EmailFactory' => __DIR__ . '/../..' . '/sources/Core/Email/EmailFactory.php', 'Combodo\\iTop\\Core\\Email\\iEMail' => __DIR__ . '/../..' . '/sources/Core/Email/iEMail.php', + 'Combodo\\iTop\\Core\\Kpi\\KpiLogData' => __DIR__ . '/../..' . '/sources/Core/Kpi/KpiLogData.php', 'Combodo\\iTop\\Core\\MetaModel\\FriendlyNameType' => __DIR__ . '/../..' . '/sources/Core/MetaModel/FriendlyNameType.php', 'Combodo\\iTop\\Core\\MetaModel\\HierarchicalKey' => __DIR__ . '/../..' . '/sources/Core/MetaModel/HierarchicalKey.php', 'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', @@ -732,6 +733,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239 'Combodo\\iTop\\Renderer\\FieldRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FieldRenderer.php', 'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php', + 'Combodo\\iTop\\Service\\Module\\ModuleService' => __DIR__ . '/../..' . '/sources/Service/Module/ModuleService.php', 'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', @@ -3312,6 +3314,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239 'iDBObjectSetIterator' => __DIR__ . '/../..' . '/core/dbobjectiterator.php', 'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php', 'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php', + 'iKPILoggerExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'iKeyboardShortcut' => __DIR__ . '/../..' . '/sources/application/UI/Hook/iKeyboardShortcut.php', 'iLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php', 'iLoginExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', diff --git a/log/web.config b/log/web.config index 58c9c3ac3..599a5f260 100644 --- a/log/web.config +++ b/log/web.config @@ -1,13 +1,8 @@ - + - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index da722b1ef..2fd094ed7 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -416,8 +416,8 @@ EOF; if ($oMysqli->connect_errno) { $sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort; - throw new BackupException("Cannot connect to the MySQL server '$sHost' (".$oMysqli->connect_errno.") ".$oMysqli->connect_error); - } + throw new MySQLException('Could not connect to the DB server '.$oMysqli->connect_errno.' (mysql errno: '.$oMysqli->connect_error, array('host' => $sHost, 'user' => $sUser)); + } if (!$oMysqli->select_db($this->sDBName)) { throw new BackupException("The database '$this->sDBName' does not seem to exist"); @@ -522,6 +522,8 @@ EOF; } /** + * Define if we should force a transport option + * * @param string $sHost * * @return string . @@ -532,6 +534,8 @@ EOF; { $sTransportOptions = ''; + /** N°6123 As we're using a --port option, if we use localhost as host, + * MariaDB > 10.6 will implicitly change its protocol from socket to tcp and throw a warning **/ if($sHost === 'localhost'){ $sTransportOptions = '--protocol=tcp'; } diff --git a/sources/Core/Kpi/KpiLogData.php b/sources/Core/Kpi/KpiLogData.php new file mode 100644 index 000000000..de39ab388 --- /dev/null +++ b/sources/Core/Kpi/KpiLogData.php @@ -0,0 +1,125 @@ +sType = $sType; + $this->sOperation = $sOperation; + $this->sArguments = @iconv(mb_detect_encoding($sArguments, mb_detect_order(), true), 'UTF-8', $sArguments); + $this->fStartTime = $fStartTime; + $this->fStopTime = $fStopTime; + $this->sExtension = $sExtension; + $this->iInitialMemory = $iInitialMemory; + $this->iCurrentMemory = $iCurrentMemory; + $this->iPeakMemory = $iPeakMemory; + $this->aData = $aData; + } + + /** + * Return the CSV Header + * + * @return string + */ + public static function GetCSVHeader() + { + return "Type,Operation,Arguments,StartTime,StopTime,Duration,Extension,InitialMemory,CurrentMemory,PeakMemory"; + } + + /** + * Return the CSV line for the values + * @return string + */ + public function GetCSV() + { + $fDuration = sprintf('%01.4f', $this->fStopTime - $this->fStartTime); + $sType = $this->RemoveQuotes($this->sType); + $sOperation = $this->RemoveQuotes($this->sOperation); + $sArguments = $this->RemoveQuotes($this->sArguments); + $sExtension = $this->RemoveQuotes($this->sExtension); + return "\"$sType\",\"$sOperation\",\"$sArguments\",$this->fStartTime,$this->fStopTime,$fDuration,\"$sExtension\",$this->iInitialMemory,$this->iCurrentMemory,$this->iPeakMemory"; + } + + private function RemoveQuotes(string $sEntry): string + { + return str_replace('"', "'", $sEntry); + } + + /** + * @param \Combodo\iTop\Core\Kpi\KpiLogData $oOther + * + * @return float + */ + public function Compare(KpiLogData $oOther): float + { + if ($oOther->fStartTime > $this->fStartTime) { + return -1; + } + return 1; + } + + public function Contains(KpiLogData $oOther): bool + { + if ($oOther->fStartTime < $this->fStartTime) { + return false; + } + + if ($oOther->fStartTime > $this->fStopTime) { + return false; + } + + return true; + } + + public function __toString() + { + return "$this->sType:$this->sOperation:$this->sArguments"; + } + + public function GetUUID(): string + { + return sha1($this->__toString()); + } +} \ No newline at end of file diff --git a/sources/Service/Module/ModuleService.php b/sources/Service/Module/ModuleService.php new file mode 100644 index 000000000..5370f7bbe --- /dev/null +++ b/sources/Service/Module/ModuleService.php @@ -0,0 +1,214 @@ +getDeclaringClass(); + $sExtension = $this->GetModuleNameFromObject($oReflectionClass->getName()); + if (strlen($sExtension) !== 0) { + $sSignature .= '['.$sExtension.'] '; + } + $sSignature .= $oReflectionClass->getShortName().'::'.$sMethod.'()'; + + return $sSignature; + } + + /** + * Get the module name from an object or class + * + * @param object|string $object + * + * @return string + * @throws \ReflectionException + */ + public function GetModuleNameFromObject($object): string + { + $oReflectionClass = new ReflectionClass($object); + $sPath = str_replace('\\', '/', $oReflectionClass->getFileName()); + $sPattern = str_replace('\\', '/', '@'.APPROOT.'env-'.utils::GetCurrentEnvironment()).'/(?.+)/@U'; + if (preg_match($sPattern, $sPath, $aMatches) !== false) { + if (isset($aMatches['ext'])) { + return $aMatches['ext']; + } + } + + return ''; + } + + /** + * **Warning** : returned result can be invalid as we're using backtrace to find the module dir name + * + * @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module + * + * @return string the relative (to MODULESROOT) path of the root directory of the module containing the file where the call to + * this function is made + * or an empty string if no such module is found (or not called within a module file) + * + * @uses \debug_backtrace() + */ + public function GetCurrentModuleDir(int $iCallDepth): string + { + $sCurrentModuleDir = ''; + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); + + foreach(GetModulesInfo() as $sModuleName => $aInfo) + { + if ($aInfo['root_dir'] !== '') + { + $sRootDir = realpath(APPROOT.$aInfo['root_dir']); + + if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) + { + $sCurrentModuleDir = basename($sRootDir); + break; + } + } + } + return $sCurrentModuleDir; + } + + /** + * **Warning** : as this method uses {@see GetCurrentModuleDir} it produces hazardous results. + * You should better uses directly {@see GetAbsoluteUrlModulesRoot} and add the module dir name yourself ! See N°4573 + * + * @return string the base URL for all files in the current module from which this method is called + * or an empty string if no such module is found (or not called within a module file) + * @throws \Exception + * + * @uses GetCurrentModuleDir + */ + public function GetCurrentModuleUrl(): string + { + $sDir = $this->GetCurrentModuleDir(1); + if ( $sDir !== '') + { + return utils::GetAbsoluteUrlModulesRoot().'/'.$sDir; + } + return ''; + } + + /** + * @param string $sProperty The name of the property to retrieve + * @param mixed $defaultValue + * + * @return mixed the value of a given setting for the current module + */ + public function GetCurrentModuleSetting(string $sProperty, $defaultValue = null) + { + $sModuleName = $this->GetCurrentModuleName(1); + return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultValue); + } + + /** + * @param string $sModuleName + * + * @return string|NULL compiled version of a given module, as it was seen by the compiler + */ + public function GetCompiledModuleVersion(string $sModuleName): ?string + { + $aModulesInfo = GetModulesInfo(); + if (array_key_exists($sModuleName, $aModulesInfo)) + { + return $aModulesInfo[$sModuleName]['version']; + } + return null; + } + + /** + * Returns the name of the module containing the file where the call to this function is made + * or an empty string if no such module is found (or not called within a module file) + * + * @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module + * + * @return string + */ + public function GetCurrentModuleName(int $iCallDepth = 0): string + { + $sCurrentModuleName = ''; + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); + + return $this->GetModuleNameFromPath($sCallerFile); + } + + private function GetModuleNameFromPath($sPath) + { + foreach (GetModulesInfo() as $sModuleName => $aInfo) { + if ($aInfo['root_dir'] !== '') { + $sRootDir = realpath(APPROOT.$aInfo['root_dir']); + if (substr($sPath, 0, strlen($sRootDir)) === $sRootDir) { + + return $sModuleName; + } + } + } + + return ''; + } + + /** + * Get the extension code from the call stack. + * Scan the call stack until a module is found. + * + * @param int $iLevelsToIgnore + * + * @return string module name + */ + public function GetModuleNameFromCallStack(int $iLevelsToIgnore = 0): string + { + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $aCallStack = array_slice($aCallStack, $iLevelsToIgnore); + + foreach ($aCallStack as $aCallInfo) { + $sFile = realpath(empty($aCallInfo['file']) ? '' : $aCallInfo['file']); + + $sModuleName = $this->GetModuleNameFromPath($sFile); + if (strlen($sModuleName) > 0) { + return $sModuleName; + } + } + + return ''; + } + +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php b/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php index 9506b5a59..f0694967b 100644 --- a/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php @@ -84,4 +84,31 @@ class DBBackupTest extends ItopTestCase $this->assertStringStartsWith(' --ssl', $sCliArgsCapathCfg); $this->assertStringEndsWith('--ssl-ca='.DBBackup::EscapeShellArg($sTestCa), $sCliArgsCapathCfg); } + + /** + * Host is localhost, we should be forced into tcp + * + * @return void + */ + public function testGetMysqlCliTransportOptionWithLocalhost() + { + $sHost= 'localhost'; + $sTransport = DBBackup::GetMysqlCliTransportOption($sHost); + + $this->assertStringStartsWith('--protocol=tcp', $sTransport); + $this->assertStringEndsWith('--protocol=tcp', $sTransport); + } + + /** + * Host is not localhost, we shouldn't be forced into tcp + * + * @return void + */ + public function testGetMysqlCliTransportOptionWithoutLocalhost() + { + $sHost= '127.0.0.1'; + $sTransport = DBBackup::GetMysqlCliTransportOption($sHost); + + $this->assertEmpty($sTransport); + } }