<?php if (!class_exists('appmonitorcheck')) { require_once 'appmonitor-checks.class.php'; } /** * ____________________________________________________________________________ * * _____ _____ __ _____ _ _ * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| * |_| |_| * _ _ _ * ___| |_|___ ___| |_ * | _| | | -_| | _| * |___|_|_|___|_|_|_| * * ____________________________________________________________________________ * * APPMONITOR :: CLASS FOR CLIENT CHECKS<br> * <br> * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br> * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br> * OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND, <br> * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED <br> * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE <br> * ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. <br> * SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY <br> * SERVICING, REPAIR OR CORRECTION.<br> * <br> * --------------------------------------------------------------------------------<br> * <br> * --- HISTORY:<br> * 2014-10-24 0.5 axel.hahn@iml.unibe.ch<br> * 2014-11-21 0.6 axel.hahn@iml.unibe.ch removed meta::ts <br> * 2018-08-23 0.50 axel.hahn@iml.unibe.ch show version<br> * 2018-08-24 0.51 axel.hahn@iml.unibe.ch method to show local status page<br> * 2018-08-27 0.52 axel.hahn@iml.unibe.ch add pdo connect (starting with mysql)<br> * 2018-11-05 0.58 axel.hahn@iml.unibe.ch additional flag in http check to show content<br> * 2019-05-31 0.87 axel.hahn@iml.unibe.ch add timeout as param in connective checks (http, tcp, databases)<br> * 2020-05-03 0.110 axel.hahn@iml.unibe.ch update renderHtmloutput<br> * 2023-07-06 0.128 axel.hahn@unibe.ch update httpcontent check<br> * 2024-07-19 0.137 axel.hahn@unibe.ch php 8 only: use typed variables * 2024-11-22 0.141 axel.hahn@unibe.ch Set client version to server version after updating http, mysqli and app checks * 2025-01-02 0.149 axel.hahn@unibe.ch add getChecks method * 2025-03-03 0.153 axel.hahn@unibe.ch fix client checks during development of a compiled binary * 2025-03-04 0.154 axel.hahn@unibe.ch finish with exitcode instead of die() * 2025-03-17 0.155 axel.hahn@unibe.ch added: getVersion() and setVersion() * 2025-03-19 0.156 axel.hahn@unibe.ch added: validation rules for parameters in all checks * 2025-03-24 0.157 axel.hahn@unibe.ch update validator for bool check in ini files * --------------------------------------------------------------------------------<br> * @version 0.157 * @author Axel Hahn * @link TODO * @license GPL * @license http://www.gnu.org/licenses/gpl-3.0.html GPL 3.0 * @package IML-Appmonitor */ class appmonitor { /** * Name and Version number * @var string */ protected string $_sVersion = '0.157'; /** * config: default ttl for server before requesting the client check again * value is in seconds * @var int */ protected int $_iDefaultTtl = 300; /** * internal counter: greatest return value of all checks * @var integer */ protected int $_iMaxResult = -1; /** * responded metadata of a website * @see _createDefaultMetadata() * @var array */ protected array $_aMeta = []; /** * Response array of all checks * @see addCheck() * @var array */ protected array $_aChecks = []; /** * for time measurements: start time * @var float */ protected float $_iStart = 0; /** * constructor: init data */ public function __construct() { $this->_createDefaultMetadata(); } // ---------------------------------------------------------------------- // protected function // ---------------------------------------------------------------------- /** * Create basic array values for metadata * @return boolean */ protected function _createDefaultMetadata(): bool { $this->_iStart = microtime(true); $this->_aMeta = [ "host" => false, "website" => false, "ttl" => false, "result" => false, "time" => false, "version" => false, ]; // fill with default values $this->setHost(); $this->setWebsite(); $this->setTTL(); $this->setVersion(); return true; } // ---------------------------------------------------------------------- // setter // ---------------------------------------------------------------------- /** * Set the physical hostname for metadata; if no host is given then * the php_uname("n") will be used to set one. * * @param string $s hostname * @return bool */ public function setHost(string $s = ''): bool { if (!$s) { $s = php_uname("n"); } if (!$s) { return false; } $this->_aMeta["host"] = $s; return true; } /** * Set final result in meta data; if no value was given then it * sets the biggest value of any check. * * @param integer $iResult set resultcode; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN * @return boolean */ public function setResult(int $iResult = -1): bool { if ($iResult === -1) { $iResult = $this->_iMaxResult; // see addCheck() } $this->_aMeta["result"] = $iResult; return true; } /** * set a ttl value in seconds to define how long a server should not * ask again for a new status of this instance * * @param int $iTTl TTL value in sec * @return boolean */ public function setTTL($iTTl = 0) { if ($iTTl == 0) { $iTTl = $this->_iDefaultTtl; } return $this->_aMeta["ttl"] = $iTTl; } /** * Set a prefix for meta -> version ... before "-php-client-v<VERSION>" * @param string $sVersionPrefix new prefix * @return bool */ public function setVersion($sVersionPrefix = ""): bool { $this->_aMeta["version"] = ($sVersionPrefix ? "$sVersionPrefix-" : "") . "php-client-v$this->_sVersion" ; return true; } /** * Set a name for this website or application and its environment * (dev, test, prod); * * If you have several application in subdirectories, i.e. /blog, /shop... * then you should the path or any description to identify them too * * if no argument is given the name of HTTP_HOST will be used * * @param string $sWebsite Name of the website or web application * @return boolean */ public function setWebsite($sWebsite = ''): bool { if (!$sWebsite && isset($_SERVER["HTTP_HOST"])) { $sWebsite = $_SERVER["HTTP_HOST"]; } if (!$sWebsite) { return false; } $this->_aMeta["website"] = $sWebsite; return true; } /** * Add a check array * @param array $aJob array with check data * @return boolean */ public function addCheck($aJob = []): bool { require_once 'appmonitor-checks.class.php'; $oCheck = new appmonitorcheck(); $aCheck = $oCheck->makecheck($aJob); // limit result code $iMyResult = isset($aJob['worstresult']) ? min($aCheck["result"], $aJob['worstresult']) : $aCheck["result"] ; if (!$this->_iMaxResult || $iMyResult > $this->_iMaxResult) { $this->_iMaxResult = $iMyResult; } $this->_aChecks[] = $aCheck; return true; } /** * Add an item to notifications meta data * @see addEmail() * @see addSlack() * * @param string $sType type ... one of email|slack * @param string $sValue value * @param string $sKey optional key (for key->value instead of list of values) * @return boolean */ protected function _addNotification(string $sType, string $sValue, string $sKey = ''): bool { $sTypeCleaned = preg_replace('/[^a-z]/', '', strtolower($sType)); if (!isset($this->_aMeta['notifications'])) { $this->_aMeta['notifications'] = []; } if (!isset($this->_aMeta['notifications'][$sTypeCleaned])) { $this->_aMeta['notifications'][$sTypeCleaned] = []; } if ($sKey) { $this->_aMeta['notifications'][$sTypeCleaned][$sKey] = $sValue; } else { $this->_aMeta['notifications'][$sTypeCleaned][] = $sValue; } return true; } /** * Add an email to notifications list * * @param string $sEmailAddress email address to add * @return boolean */ public function addEmail(string $sEmailAddress) { return $this->_addNotification('email', $sEmailAddress); } /** * Add slack channel for notification * @param string $sLabel * @param string $sSlackWebhookUrl * @return boolean */ public function addSlackWebhook(string $sLabel, string $sSlackWebhookUrl): bool { return $this->_addNotification('slack', $sSlackWebhookUrl, $sLabel); } /** * Add a tag for grouping in the server gui. * Spaces will be replaced with underscore * * @param string $sTag tag to add * @return boolean */ public function addTag(string $sTag): bool { if (!isset($this->_aMeta['tags'])) { $this->_aMeta['tags'] = []; } $this->_aMeta['tags'][] = str_replace(' ', '_', $sTag); return true; } /** * Check referers IP address if it matches any entry in the list * requires http request; CLI is always allowed * On deny this method exits with 403 response * * @param array $aAllowedIps array of allowed ip addresses / ranges * the ip must match from the beginning, i.e. * "127.0." will allow requests from 127.0.X.Y * @return boolean */ public function checkIp(array $aAllowedIps = []): bool { if (!isset($_SERVER['REMOTE_ADDR']) || !count($aAllowedIps)) { return true; } $sIP = $_SERVER['REMOTE_ADDR']; foreach ($aAllowedIps as $sIp2Check) { if (strpos($sIP, $sIp2Check) === 0) { return true; } } $this->_exit( 403, 'ERROR: Your ip address [' . $sIP . '] has no access.', 11 ); } /** * Check a token * requires http request; CLI is always allowed * On deny this method exits with 403 response * * @param string $sVarname name of GET variable * @param string $sToken value * @return boolean */ public function checkToken(string $sVarname, string $sToken): bool { if (!isset($_GET)) { return true; } if (isset($_GET[$sVarname]) && $_GET[$sVarname] === $sToken) { return true; } $this->_exit( 403, 'ERROR: A token is required.', 12 ); } // ---------------------------------------------------------------------- // getter // ---------------------------------------------------------------------- /** * Get version of the appmoinitor php client * @return string */ public function getVersion(): string { return $this->_sVersion; } /** * list all available check functions. This is a helper class you cann call * to get an overview over built in functions. You get a flat array with * all function names. * @return array */ public function listChecks(): array { require_once 'appmonitor-checks.class.php'; $oCheck = new appmonitorcheck(); return $oCheck->listChecks(); } // ---------------------------------------------------------------------- // checks // ---------------------------------------------------------------------- /** * verify array values and in case of an error abort and show all found errors * @return boolean */ protected function _checkData(): bool { $aErrors = []; if (!count($this->_aChecks)) { $aErrors[] = "No checks have been defined."; } if ($this->_aMeta["result"] === false) { $aErrors[] = "method setResult was not used to set a final result for all checks."; } if (count($aErrors)) { $this->_exit( 503, 'Client check is not complete<br>Found errors:<br>' . '<ol><li>' . implode('<li>', $aErrors) . '</ol><br><br>', 10 ); } return true; } // ---------------------------------------------------------------------- // output // ---------------------------------------------------------------------- /** * Exit execution with error message and given exicode. * @param int $iHttpcode http statuscode * @param string $sMessage detailed message * @param int $iExitcode exitcode * @return never */ protected function _exit($iHttpcode, $sMessage, $iExitcode): never { $aStatus = [ 403 => 'Forbidden', 503 => 'Service Unavailable', ]; header("HTTP/1.1 $iHttpcode $aStatus[$iHttpcode]"); echo "<h1>$iHttpcode $aStatus[$iHttpcode]</h1> <h2>Details</h2> $sMessage\n"; exit($iExitcode); } /** * Get array with executed checks * @return array */ public function getChecks(): array { return $this->_aChecks; } /** * Get full array for response with metadata and checks * @return array */ public function getResults(): array { return [ "meta" => $this->_aMeta, "checks" => $this->_aChecks, ]; } /** * Send http response with header and appmonitor JSON data * @return string */ public function render(): string { $this->_checkData(); $this->_aMeta['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; $sOut = json_encode($this->getResults()); header('Content-type: application/json'); header('Cache-Control: cache'); header('max-age: ' . $this->_aMeta["ttl"]); echo $sOut; return $sOut; } /** * Output appmonitor client status as single html page * * @example <code> * ob_start();<br> * require __DIR__ . '/appmonitor/client/index.php'; * $sJson=ob_get_contents(); * ob_end_clean(); * $oMonitor->renderHtmloutput($sJson); * </code> * * @param string $sJson JSON of client output * @return string */ public function renderHtmloutput(string $sJson): string { header('Content-type: text/html'); header('Cache-Control: cache'); header('max-age: ' . $this->_aMeta["ttl"]); $aMsg = [ 0 => "OK", 1 => "UNKNOWN", 2 => "WARNING", 3 => "ERROR" ]; // $sOut = print_r($sJson, 1); $aData = json_decode($sJson, 1); // ----- Ausgabe human readable $sOut = ''; $sOut .= '' . '<h2>Metadata</h2>' . '<div class="meta' . (isset($aData['meta']['result']) ? ' result' . $aData['meta']['result'] : '') . '">' . 'Status: ' . (isset($aData['meta']['result']) ? $aMsg[$aData['meta']['result']] : '?') . '<br>' . '</div>' . 'Host: ' . (isset($aData['meta']['host']) ? '<span class="string">' . $aData['meta']['host'] . '</span>' : '?') . '<br>' . 'Website: ' . (isset($aData['meta']['website']) ? '<span class="string">' . $aData['meta']['website'] . '</span>' : '?') . '<br>' . 'Execution time: ' . (isset($aData['meta']['time']) ? '<span class="float">' . $aData['meta']['time'] . '</span>' : '?') . '<br>' . 'Client: ' . (isset($aData['meta']['version']) ? '<span class="string">' . $aData['meta']['version'] . '</span>' : '?') . '<br>' . '<h2>Checks</h2>' ; if (isset($aData['checks'][0]) && count($aData['checks'])) { foreach ($aData['checks'] as $aCheck) { $sOut .= '' . '<span class="result' . $aCheck['result'] . '"> <strong>' . $aCheck['name'] . '</strong></span> <br>' . '<div class="check">' . '<div class="description">' . $aCheck['description'] . '<br>' . $aCheck['value'] . '<br>' . '</div>' . 'Execution time: <span class="float">' . (isset($aCheck['time']) ? $aCheck['time'] : ' - ') . '</span><br>' . 'Group: <span class="string">' . (isset($aCheck['group']) ? $aCheck['group'] : '-') . '</span><br>' . 'parent: <span class="string">' . (isset($aCheck['parent']) ? $aCheck['parent'] : '-') . '</span><br>' . 'Status: ' . $aMsg[$aCheck['result']] . '<br>' . '</div>' ; } } $sOut .= '<h2>List of farbcodes</h2>'; foreach ($aMsg as $i => $sText) { $sOut .= '<span class="result' . $i . '">' . $sText . '</span> '; } $sRaw = json_encode($aData, JSON_PRETTY_PRINT); $sRaw = preg_replace('/:\ \"(.*)\"/U', ': "<span class="string">$1</span>"', $sRaw); $sRaw = preg_replace('/:\ ([0-9]*)/', ': <span class="int">$1</span>', $sRaw); $sRaw = preg_replace('/\"(.*)\":/U', '"<span class="key">$1</span>":', $sRaw); $sOut .= '<h2>Raw result data</h2><pre id="raw">' . $sRaw . '</pre>'; $sOut = '<!DOCTYPE html><html><head>' . '<style>' . 'body{background:#eee; color:#444; font-family: verdana,arial; margin: 0; }' . 'body>div#content{background: #fff; border-radius: 2em; border: 4px solid #abc; box-shadow: 0.5em 0.5em 2em #aaa; margin: 2em 10%; padding: 2em;}' . 'h1{color:#346; margin: 0;}' . 'h2{color:#569; margin-top: 2em;}' . 'pre{background:#f4f4f8; padding: 1em; overflow-x:auto; }' . '#raw .key{color:#808;}' . '#raw .int{color:#3a3; font-weight: bold;}' . '#raw .string{color:#66e;}' . '.check{border: 1px solid #ccc; padding: 0.4em; margin-bottom: 2em;}' . '.description{font-style: italic; padding: 0.4em 1em;}' . '.float{color:#080;}' . '.meta{margin-bottom: 1em;}' . '.result0{background:#aca; border-left: 1em solid #080; padding: 0.5em; }' . '.result1{background:#ccc; border-left: 1em solid #aaa; padding: 0.5em; }' . '.result2{background:#fc9; border-left: 1em solid #860; padding: 0.5em; }' . '.result3{background:#f88; border-left: 1em solid #f00; padding: 0.5em; }' . '.string{color:#338;}' . '</style>' . '<title>' . __CLASS__ . '</title>' . '</head><body>' . '<div id="content">' . '<h1>' . __CLASS__ . ' :: client status</h1>' . $sOut . '</div>' . '</body></html>'; return $sOut; } }