<?php /** * ______________________________________________________________________ * * _____ _ _ _ * / __ \ (_) | | (_) * | / \/_ __ ___ _ __ _ ___ | |__ __ ___ _____ _____ _ __ * | | | '__/ _ \| '_ \| |/ _ \| '_ \ \ \ / / |/ _ \ \ /\ / / _ \ '__| * | \__/\ | | (_) | | | | | (_) | |_) | \ V /| | __/\ V V / __/ | * \____/_| \___/|_| |_| |\___/|_.__/ \_/ |_|\___| \_/\_/ \___|_| * _/ | * |__/ * ______________________________________________________________________ * * The cronjob viewer for centralized monitoring of cronjobs using * Axels cronrwapper (see <https://github.com/axelhahn/cronwrapper>). * * You can browse all servers to see * - the last status of all cronjobs * - results an execution times of running jonbs * - a timeline for all jobs running > 60s * * * Free software. Open Source. GNU GPL 3. * SOURCE: <https://git-repo.iml.unibe.ch/iml-open-source/cronlog-viewer> * * ______________________________________________________________________ * * The cronlog class contains non visual methods * @see cronlog-renderer.class.php * * @license GNU GPL 3.0 * @author Axel Hahn <axel.hahn@iml.unibe.ch> * * 2024-09-20 <axel.hahn@unibe.ch> added type declarations; update php docs */ class cronlog { /** * Data dir with subdirs per host and its cronjob logs * @var string */ protected string $_sDataDir = "__APPDIR__/data"; /** * TTL for cached data * @var int */ protected int $_iTtlCache = 60; // in sec /** * When show an error for expired jobs (keep in mind the latency to execute job and sync logs) * @var integer */ protected int $_iExpiredJobsFailAfter = 60 * 30; // in sec /** * Minimal TTL for a cronjob * @var int */ protected int $_iMinTtl = 0; // in sec /** * List of cronjobs to hide in the web view * @var array */ protected array $_aSkipJoblogs = []; protected array $_aInstances = []; protected array $_aServers = []; protected string $_sActiveServer = ''; protected string $_sFileFilter_serverjoblog = '*joblog*.done'; protected string $_sFileFilter_joblog = '*.log'; protected string $_sFileFilter_jobrunning = '*.log.running*'; protected string $_sLang = ''; // language ... read from config file protected array $_aLang = []; // language data // ---------------------------------------------------------------------- // MAIN // ---------------------------------------------------------------------- /** * Constructor */ public function __construct() { // read config if (file_exists(__DIR__ . '/../config/inc_cronlog.php')) { $aCfgTemp = include(__DIR__ . '/../config/inc_cronlog.php'); $this->_sDataDir = isset($aCfgTemp['sDatadir']) ? $aCfgTemp['sDatadir'] : $this->_sDataDir; $this->_iTtlCache = isset($aCfgTemp['iTtlCache']) ? (int) $aCfgTemp['iTtlCache'] : $this->_iTtlCache; $this->_iMinTtl = isset($aCfgTemp['iMinTtl']) ? (int) $aCfgTemp['iMinTtl'] : $this->_iMinTtl; $this->_iExpiredJobsFailAfter = isset($aCfgTemp['iExpiredJobsFailAfter']) ? (int) $aCfgTemp['iExpiredJobsFailAfter'] : $this->_iExpiredJobsFailAfter; $this->_aSkipJoblogs = isset($aCfgTemp['aHidelogs']) && is_array($aCfgTemp['aHidelogs']) ? $aCfgTemp['aHidelogs'] : $this->_aSkipJoblogs; $this->_aInstances = isset($aCfgTemp['instances']) ? $aCfgTemp['instances'] : []; $this->_sLang = isset($aCfgTemp['lang']) && $aCfgTemp['lang'] ? $aCfgTemp['lang'] : 'en-en'; if (!file_exists(__DIR__ . '/../config/lang_' . $this->_sLang . '.php')) { header('HTTP/1.1 503 Service Temporarily Unavailable'); header('Status: 503 Service Temporarily Unavailable'); die('ERROR: lang file for lang => "' . $this->_sLang . '" not found.<br>config/lang_' . $this->_sLang . '.php<br>does not exist.'); } $this->_aLang = $aCfgTemp = include(__DIR__ . '/../config/lang_' . $this->_sLang . '.php'); } $this->_sDataDir = str_replace("__APPDIR__", dirname(dirname(__FILE__)), $this->_sDataDir); $this->_sDataDir = str_replace('\\', '/', $this->_sDataDir); $this->getServers(); } // ---------------------------------------------------------------------- // private // ---------------------------------------------------------------------- /** * chaching: get the full path of directory for caching * @return string */ protected function _getCacheDir(): string { return $this->_sDataDir . '/__cache'; } /** * Caching: get full path of a caching item * @param string $sTaskId Name of a task * @return string */ protected function _getCacheFile(string $sTaskId): string { return $this->_getCacheDir() . '/' . $sTaskId; } /** * Read logs: get full path to a servers cronjob logdata * @return string */ protected function _getServerlogDir(): string { return $this->_sDataDir . '/' . $this->_sActiveServer; } /** * Caching: get cached data if they exist and aren't expired * @param string $sTaskId Name of the task * @return mixed string|array */ protected function _getCacheData(string $sTaskId): string|array { // DISABLE CACHE return false; $sFile = $this->_getCacheFile($sTaskId); if (file_exists($sFile)) { if (filemtime($sFile) > (date('U') - $this->_iTtlCache)) { // echo "USE cache $sFile<br>"; return unserialize(file_get_contents($sFile)); } else { // echo "DELETE cache $sFile<br>"; unlink($sFile); } } return ''; } /** * Read logs: parse a single line in the joblog and return has with all key value items * @param string $sLine single line in the log * @return array */ protected function _parseJoblogLine(string $sLine): array { $aReturn = []; // echo "DEBUG $sLine<br>"; // job=dok-kvm-instances:host=kalium:start=1538358001:end=1538358001:exectime=0:ttl=60:rc=0 $sLine = str_replace("\n", '', $sLine); $sLine = str_replace(':', '", "', $sLine); $sLine = str_replace('=', '": "', $sLine); $sLine = '{"' . $sLine . '"}'; // echo "DEBUG $sLine<br><br>"; $aReturn = json_decode($sLine, 1); if (!is_array($aReturn)) { echo "not a JSON string<br>"; echo "DEBUG $sLine<br><br>"; die(); } return $aReturn; } /** * Read logs: parse the whole cronwrapper logfile and return a hash * @param string $sFile filename with full path * @return array */ protected function _parseLogfile(string $sFile): array { $aReturn = [ 'SCRIPTNAME' => false, 'SCRIPTTTL' => false, 'SCRIPTSTARTTIME' => false, 'SCRIPTLABEL' => false, 'SCRIPTENDTIME' => false, 'SCRIPTEXECTIME' => false, 'SCRIPTRC' => false, // 'SCRIPTOUT'=>[], ]; $fileHandle = fopen($sFile, "r"); while (($line = fgets($fileHandle)) !== false) { // get key ... the part before "=" $sKey = trim(preg_replace('/=.*/', '', $line)); if ($sKey && isset($aReturn[$sKey])) { // add value ... the part behind "=" $aReturn[$sKey] = preg_replace('/^([A-Z]*\=)/', '', $line); } } fclose($fileHandle); // fetch unit timestamp from date values (they are like "2018-09-30 03:40:05, 1538278805") $aReturn['SCRIPTSTARTTIME'] = (int) preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']); $aReturn['SCRIPTENDTIME'] = (int) preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']); // remove " s" from exec time value $aReturn['SCRIPTEXECTIME'] = preg_replace('/\ s$/', '', $aReturn['SCRIPTEXECTIME']); return $aReturn; } /** * caching: write new data; it returns the success of write operation as bool * @param string $sTaskId * @param mixed $data data to store; can be any serializable value * @return boolean|integer */ protected function _writeCacheData(string $sTaskId, mixed $data): bool|int { $sFile = $this->_getCacheFile($sTaskId); // echo "WRITE cache $sFile<br>"; return file_put_contents($sFile, serialize($data)); } // ---------------------------------------------------------------------- // public getter // ---------------------------------------------------------------------- /** * Get currently selected server * @return string */ public function getServer(): string { return $this->_sActiveServer; } /** * Get array with existing servers in data dir * @return array */ public function getServers(): array { if (is_array($this->_aServers) && count($this->_aServers)) { return $this->_aServers; } $this->_aServers = []; // echo "DEBUG DATADIR: " . $this->_sDataDir."<br>"; if (!is_dir($this->_sDataDir)) { echo "WARNING: no data. Check sDatadir in the config and set it to an existing directory.<br>"; die(); } if ($handle = opendir($this->_sDataDir)) { while (false !== ($entry = readdir($handle))) { if ($entry != "." && $entry != ".." && $entry != "__cache" && is_dir($this->_sDataDir . '/' . $entry)) { // echo "DEBUG $entry<br>\n"; $this->_aServers[$entry] = []; } } closedir($handle); } ksort($this->_aServers); return $this->_aServers; } /** * Get logs from jobfilea of the current or given server * @param boolean $bUseSkip hide jobs if their label matches the skip list; default: true * @return array */ public function getServerJobHistory(bool $bUseSkip = true): bool|array { $aReturn = []; $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; $aData = $this->_getCacheData($sTaskId); if ($aData) { return $aData; } $aData = []; foreach (glob($this->_getServerlogDir() . '/' . $this->_sFileFilter_serverjoblog) as $sMyJobfile) { // echo "DEBUG: $sMyJobfile<br>"; $fileHandle = fopen($sMyJobfile, "r"); while (($line = fgets($fileHandle)) !== false) { // send the current file part to the browser $aData = $this->_parseJoblogLine($line); if (!$bUseSkip || array_search($aData['job'], $this->_aSkipJoblogs) === false) { $aReturn[$aData['start'].'__'.$aData['host'].'__'.$aData['job']] = $aData; } } fclose($fileHandle); } krsort($aReturn); $this->_writeCacheData($sTaskId, $aReturn); return $aReturn; } /** * Get logs from jobfilea of the current or given server * @return array */ public function getServersLastLog(): array { $aReturn = []; $aData = []; foreach (glob($this->_getServerlogDir() . '/' . $this->_sFileFilter_joblog) as $sMyJobfile) { // echo "DEBUG: log file $sMyJobfile<br>"; $aData = $this->_parseLogfile($sMyJobfile); $aData['server'] = $this->_sActiveServer; $aData['logfile'] = $this->_sActiveServer . '/' . basename($sMyJobfile); $aReturn[$aData['SCRIPTSTARTTIME'] . $sMyJobfile] = $aData; } rsort($aReturn); return $aReturn; } /** * get logs from jobfilea of the current or given server * @return array */ public function getRunningJobs(): array { $aReturn = []; $aData = []; foreach (glob($this->_getServerlogDir() . '/' . $this->_sFileFilter_jobrunning) as $sMyJobfile) { // echo "DEBUG: log file $sMyJobfile<br>"; $aData = $this->_parseLogfile($sMyJobfile); $aData['server'] = $this->_sActiveServer; $aData['logfile'] = $this->_sActiveServer . '/' . basename($sMyJobfile); $aReturn[$aData['SCRIPTSTARTTIME'] . $sMyJobfile] = $aData; } rsort($aReturn); return $aReturn; } /** * Translate: get a language specific text of a given key * @param string $id id of language text * @return string */ public function t($id): string { return $this->_aLang[$id] ?? '[' . $id . '] ???'; } // ---------------------------------------------------------------------- // public setter // ---------------------------------------------------------------------- /** * Set which server is selected * The given server must exist as directory (that contains its logs) * It returns false if the given server does not exist or has value 'ALL' * @param string $sServer server name * @return bool|string */ public function setServer(string $sServer): bool|string { $this->_sActiveServer = false; if ($sServer === 'ALL') { return false; } if ($sServer && !array_key_exists($sServer, $this->_aServers)) { echo "WARNING: server [$sServer] does not exist<br>"; return false; } $this->_sActiveServer = $sServer; return $this->_sActiveServer; } }