Select Git revision
cronlog.class.php
-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
cronlog.class.php 14.15 KiB
<?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;
}
}