diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index a007a3ab3..e6eb02407 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -2246,3 +2246,27 @@ 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 6fb1987ad..070eef921 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -4547,7 +4547,9 @@ HTML; foreach (MetaModel::EnumPlugins(iApplicationObjectExtension::class) as $oExtensionInstance) { $sExtensionClass = get_class($oExtensionInstance); $this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBInsert()"); + $oKPI = new ExecutionKPI(); $oExtensionInstance->OnDBInsert($this, self::GetCurrentChange()); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert'); } } @@ -4562,13 +4564,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; @@ -4605,7 +4610,9 @@ HTML; foreach (MetaModel::EnumPlugins(iApplicationObjectExtension::class) as $oExtensionInstance) { $sExtensionClass = get_class($oExtensionInstance); $this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBUpdate()"); + $oKPI = new ExecutionKPI(); $oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange()); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBUpdate'); } } @@ -4649,7 +4656,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); @@ -4668,7 +4677,10 @@ HTML; foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { $sExtensionClass = get_class($oExtensionInstance); - if ($oExtensionInstance->OnIsModified($this)) { + $oKPI = new ExecutionKPI(); + $bIsModified = $oExtensionInstance->OnIsModified($this); + $oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnIsModified'); + if ($bIsModified) { $this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnIsModified() -> true"); return true; } else { @@ -4724,7 +4736,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); @@ -4772,7 +4786,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 f5c2b54ba..0b2c492a5 100644 --- a/application/startup.inc.php +++ b/application/startup.inc.php @@ -99,4 +99,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 6d6e204c0..229e6b5ba 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; @@ -2265,24 +2266,7 @@ SQL; */ 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); } /** @@ -2304,24 +2288,7 @@ SQL; */ 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); } /** @@ -2336,12 +2303,7 @@ SQL; */ public static function GetCurrentModuleUrl() { - $sDir = static::GetCurrentModuleDir(1); - if ( $sDir !== '') - { - return static::GetAbsoluteUrlModulesRoot().'/'.$sDir; - } - return ''; + return ModuleService::GetInstance()->GetCurrentModuleUrl(); } /** @@ -2351,8 +2313,7 @@ SQL; */ public static function GetCurrentModuleSetting($sProperty, $defaultvalue = null) { - $sModuleName = static::GetCurrentModuleName(1); - return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue); + return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue); } /** @@ -2361,12 +2322,7 @@ SQL; */ public static function GetCompiledModuleVersion($sModuleName) { - $aModulesInfo = GetModulesInfo(); - if (array_key_exists($sModuleName, $aModulesInfo)) - { - return $aModulesInfo[$sModuleName]['version']; - } - return null; + return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName); } /** @@ -2925,7 +2881,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 4f7fdb033..fbad4e0e5 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 6efae7642..ece53ea7e 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 d98092da0..2c45f4a90 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -607,8 +607,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 ebfa48910..f9eb40627 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -656,22 +656,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', @@ -1069,6 +1069,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 536919faa..58787fbf8 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 5b1e3dd18..9673f79d1 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1144,7 +1144,9 @@ abstract class DBObject implements iDisplay return; //skip! } $this->FireEventComputeValues(); + $oKPI = new ExecutionKPI(); $this->ComputeValues(); + $oKPI->ComputeStatsForExtension($this, 'ComputeValues'); } /** @@ -2480,7 +2482,6 @@ abstract class DBObject implements iDisplay { $this->m_aCheckIssues = array(); - $oKPI = new ExecutionKPI(); if ($bDoComputeValues) { $this->DoComputeValues(); } @@ -2490,8 +2491,9 @@ abstract class DBObject implements iDisplay $this->FireEventCheckToWrite(); $this->SetReadWrite(); + $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; @@ -3114,7 +3116,9 @@ abstract class DBObject implements iDisplay // Ensure the update of the values (we are accessing the data directly) $this->DoComputeValues(); + $oKPI = new ExecutionKPI(); $this->OnInsert(); + $oKPI->ComputeStatsForExtension($this, 'OnInsert'); $this->FireEventBeforeWrite(); @@ -3170,7 +3174,9 @@ abstract class DBObject implements iDisplay $this->DBInsertSingleTable($sParentClass); } + $oKPI = new ExecutionKPI(); $this->OnObjectKeyReady(); + $oKPI->ComputeStatsForExtension($this, 'OnObjectKeyReady'); $this->UpdateCurrentObjectInCrudStack(); $this->DBWriteLinks(); @@ -3247,7 +3253,9 @@ abstract class DBObject implements iDisplay public function PostInsertActions(): void { $this->FireEventAfterWrite([], true); + $oKPI = new ExecutionKPI(); $this->AfterInsert(); + $oKPI->ComputeStatsForExtension($this, 'AfterInsert'); // Activate any existing trigger $sClass = get_class($this); @@ -3345,7 +3353,9 @@ abstract class DBObject implements iDisplay try { $this->DoComputeValues(); $this->ComputeStopWatchesDeadline(false); + $oKPI = new ExecutionKPI(); $this->OnUpdate(); + $oKPI->ComputeStatsForExtension($this, 'OnUpdate'); $this->FireEventBeforeWrite(); @@ -3559,7 +3569,9 @@ abstract class DBObject implements iDisplay public function PostUpdateActions(array $aChanges): void { $this->FireEventAfterWrite($aChanges, false); + $oKPI = new ExecutionKPI(); $this->AfterUpdate(); + $oKPI->ComputeStatsForExtension($this, 'AfterUpdate'); // - TriggerOnObjectUpdate $aParams = array('class_list' => MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL)); @@ -3786,7 +3798,9 @@ abstract class DBObject implements iDisplay return; } + $oKPI = new ExecutionKPI(); $this->OnDelete(); + $oKPI->ComputeStatsForExtension($this, 'OnDelete'); // Activate any existing trigger $sClass = get_class($this); @@ -3894,7 +3908,9 @@ abstract class DBObject implements iDisplay } $this->FireEventAfterDelete(); + $oKPI = new ExecutionKPI(); $this->AfterDelete(); + $oKPI->ComputeStatsForExtension($this, 'AfterDelete'); $this->m_bIsInDB = false; diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index 84495097e..cf69e9425 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 8f29bbc8f..460b5a822 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) - { - 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 7e3b8cdc8..a2a9b67a5 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -6376,7 +6376,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'); @@ -6495,6 +6497,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 cc5461bab..27e68a2ed 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 aefbc6050..b0222184c 100644 --- a/core/trigger.class.inc.php +++ b/core/trigger.class.inc.php @@ -121,7 +121,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 72e675416..54317abd2 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -383,6 +383,7 @@ return array( '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\\EventListener\\AttributeBlobEventListener' => $baseDir . '/sources/Core/EventListener/AttributeBlobEventListener.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', @@ -461,6 +462,7 @@ return array( 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => $baseDir . '/sources/Service/Links/LinkSetModel.php', 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => $baseDir . '/sources/Service/Links/LinkSetRepository.php', 'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => $baseDir . '/sources/Service/Links/LinksBulkDataPostProcessor.php', + 'Combodo\\iTop\\Service\\Module\\ModuleService' => $baseDir . '/sources/Service/Module/ModuleService.php', 'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php', 'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php', 'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php', @@ -2952,6 +2954,7 @@ return array( 'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php', 'iDisplay' => $baseDir . '/core/dbobject.class.php', 'iFieldRendererMappingsExtension' => $baseDir . '/application/applicationextension.inc.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 ce4ca190b..346658c7b 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -747,6 +747,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f '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\\EventListener\\AttributeBlobEventListener' => __DIR__ . '/../..' . '/sources/Core/EventListener/AttributeBlobEventListener.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', @@ -825,6 +826,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetModel.php', 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetRepository.php', 'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Links/LinksBulkDataPostProcessor.php', + 'Combodo\\iTop\\Service\\Module\\ModuleService' => __DIR__ . '/../..' . '/sources/Service/Module/ModuleService.php', 'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php', 'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php', 'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php', @@ -3316,6 +3318,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php', 'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php', 'iFieldRendererMappingsExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.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 0a5dc7c98..f51b7525a 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -468,8 +468,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"); @@ -578,6 +578,8 @@ EOF; } /** + * Define if we should force a transport option + * * @param string $sHost * * @return string . @@ -588,6 +590,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/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php index e7c3c5a7a..d2af57229 100644 --- a/synchro/synchrodatasource.class.inc.php +++ b/synchro/synchrodatasource.class.inc.php @@ -2162,7 +2162,9 @@ class SynchroReplica extends DBObject implements iDisplay // it will be deleted by the mean of a trigger too protected function DBDeleteSingleObject() { + $oKPI = new ExecutionKPI(); $this->OnDelete(); + $oKPI->ComputeStatsForExtension($this, 'OnDelete'); if (!MetaModel::DBIsReadOnly()) { diff --git a/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php b/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php index c772d0223..715a6173b 100644 --- a/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php @@ -108,6 +108,33 @@ class DBBackupTest extends ItopTestCase $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); + } + /** * @dataProvider MakeNameProvider * @covers \DBBackup::MakeName