<?php
//lerts
//v0.25 - prototype started 01-24-2024 - DS
//v0.30 - added email suppression after X checks and clear email 02-12-2025 - DS

//Overall idea:
//This is a seedling for what will eventually be a server monitoring system that runs with zerolith and has a cute GUI etc.
//About $set: it's designed to be generated by a database driven system in the future.
//The internal data structures mirror and are based on $set as a performance cheat ( no need to make more array structures )
//In order to add additional monitoring, you just need to make sure the appropriate check is in the per aspect loops.

//'lerts is significantly overbuilt because there may be 100 different types of checks and statistics it can monitor.
//'lerts nano - just checks for failures to any criteria and nags the sysadmin via email if any are met.
//'lerts mini - writes statistics to file based on settings and has a GUI to view them.
//'lerts mondo - can submit statistics and alerts to a centralized server; 1 way communication from server to centralized server

//todo: add file locking to prevent multiple instances running during heavy load
//todo: write a rolling statistics file for X number of days, which is periodically averaged and compiled into a shorter file
//todo: add warning when disk FS doesn't match checks ( can happen when trailing / is added )

require "../../zl_init.php";
zl::$set['mailOn'] = true; //force mail on regardless of existing setting


//------------ initialization


if(php_sapi_name() == "cli") //CLI mode
{
    $cli = true;
    zl::setOutFormat("api");
    $lb = "\n"; //appropriate linebreak
}
else //page output mode
{
    $cli = false;
    zl::setDebugLevel(2); //force basic debugging on
    zl::setOutFormat("page");
    zl::$page['wrap'] = true;
    $lb = "<br>\n"; //appropriate linebreak
}

//email settings
$emailSet = ['noEmailAfterChecks' => '10', '', 'emailAfterCleared' => true];
$emailSubjectPrefix = "[lerts] " . gethostname() . " - ";

//alarms list
//set your alerts in format: $set[sysAspect], $checkDetails = [['condition1' => "90", 'condition2' => "yeah"]]
$set = [];
$set['disk'] = [["whenPct" => 90, "afterNumChecks" => 5, "fs" => "/dev/root"]];
$set['cpu']  = [["whenPct" => 95, "afterNumChecks" => 5]];
$set['mem']  = [["whenPct" => 95, "afterNumChecks" => 10], ["whenPct" => 85, "afterNumChecks" => 240]];

//for testing
//$set['disk'] = [["whenPct" => 90, "afterNumChecks" => 5, "fs" => "/dev/root"]];
//$set['cpu']  = [["whenPct" => -1, "afterNumChecks" => 5]];
//$set['mem']  = [["whenPct" => 95, "afterNumChecks" => 10], ["whenPct" => 90, "afterNumChecks" => 240]];


//make alert arrays based on $set.
$alerts = [];
foreach(array_keys($set) as $aspect) { $alerts[$aspect] = []; }
$stats = $alerts; //copy it to make the template for stats ( current stats ) array.


//------------ sanity checks / stat file creation


//load alert history
$resetTimersMSG = "";
if(!file_exists("lerts_lastStat"))  { $lastStat = createLastStat($set); } //file not found? create $lastStat from scratch
else //attempt to load existing file
{
	$lastStat = unserialize(file_get_contents("lerts_lastStat"));
	$different = false;

    //this check is necessary; if the user changes the checks in $set, we should nuke the file and start again.
	//does the file match the expected format, with the exception of the checkFails field?
	foreach($set as $aspect => $conditions) //add checkFails = 0
	{
		$c = count($conditions);
		for($i = 0; $i < $c; $i++) //iterate all the conditions
		{
			if(isset($lastStat[$aspect][$i]))
			{
				foreach($set[$aspect][$i] as $key => $value)
				{
					if($key != "checkFails")
					{
						if(isset($lastStat[$aspect][$i][$key]) && $lastStat[$aspect][$i][$key] == $set[$aspect][$i][$key]) {}
						else { $different = true; break; } //it's different
					}
				}
			}
			else { $different = true; break; }  //it's different
		}
	}
	
	//reset timers and write new file
	if($different)
    {
        $lastStat = createLastStat($set);
        $resetTimersMSG =  "Note: timers were reset due to configuration change.$lb";
    }
}

//create the partially filled out lastStat array from the settings array ($set)
function createLastStat($set)
{
	$lastStat = $set;
	foreach($set as $aspect => $conditions) //add checkFails = 0
	{
		$c = count($conditions);
		for($i = 0; $i < $c; $i++) { $lastStat[$aspect][$i]['checkFails'] = 0; }
	}
	return $lastStat;
}


//------------ stat crunching and checking


//run the checks per aspect on and add to lastStat
foreach($set as $aspect => $conditions)
{
	//which aspect?
	if($aspect == "disk")
	{
		$stats[$aspect] = zsys::getDiskSpace();
		$c = count($conditions);
		for($i = 0; $i < $c; $i++)
		{
			foreach($stats[$aspect] as $volStat) //multiple items
			{
				//look at disk usage over %
				if($volStat['fs'] == $conditions[$i]['fs'] && $volStat['usedPct'] >= $conditions[$i]['whenPct'])
				{
					$lastStat[$aspect][$i]['checkFails']++;
					
					if($lastStat[$aspect][$i]['checkFails'] >= $conditions[$i]['afterNumChecks'])
					{ $alerts[$aspect][] = "The " . $volStat['fs'] . " volume is >= " . $conditions[$i]['whenPct'] . "% full"; }
				}
				else { $lastStat[$aspect][$i]['checkFails'] = 0; }
			}
		}
	}
	else if($aspect == "cpu")
	{
		$stats[$aspect] = zsys::getCpuUsedPct();
		$c = count($conditions);
		for($i = 0; $i < $c; $i++)
		{
			//cpu over % for X seconds
			if($stats[$aspect] >= $conditions[$i]['whenPct'])
			{
				$lastStat[$aspect][$i]['checkFails']++;
				
				if($lastStat[$aspect][$i]['checkFails'] >= $conditions[$i]['afterNumChecks'])
				{ $alerts[$aspect][] = 'The cpu has been over ' . $conditions[$i]['whenPct'] . "% load for longer than " . $conditions[$i]['afterNumChecks'] . " seconds."; }
			}
			else { $lastStat[$aspect][$i]['checkFails'] = 0; }
		}
	}
	else if($aspect == "mem")
	{
		$stats[$aspect] = zsys::getMemUsed();
		$c = count($conditions);
		for($i = 0; $i < $c; $i++)
		{
			//mem usage over % for X seconds
			if($stats[$aspect]['usedPct'] >= $conditions[$i]['whenPct'])
			{
				$lastStat[$aspect][$i]['checkFails']++;
				
				if($lastStat[$aspect][$i]['checkFails'] >= $conditions[$i]['afterNumChecks'])
				{ $alerts[$aspect][] = 'The mem has been over ' . $conditions[$i]['whenPct'] . "% used for longer than " . $conditions[$i]['afterNumChecks'] . " seconds."; }
			}
			else { $lastStat[$aspect][$i]['checkFails'] = 0; }
		}
	}
}


//------------ calculate alerts and write data


//compile alerts email into human readable text
$alertMSG = "";
foreach($alerts as $type => $text)
{
    if($alerts[$type] != []) //compile alerts
    { foreach($alerts[$type] as $alertText) { $alertMSG .= $alertText . $lb;}  }
}

//write emailCounter file if it hasn't been written to before.
//emailCounter keeps track of how many times we've nagged the user
$clearedMSG = "";
if(!file_exists("lerts_emailCounter")) { $emailCounter = 0; }
else
{
    $emailCounter = file_get_contents("lerts_emailCounter");
    if($alertMSG != "") { $emailCounter++; }
    elseif($emailCounter != 0) //if no alert after a series of alerts, reset counter
    {
        $emailCounter = 0;
        if($emailSet['emailAfterCleared'])
        {
            //zmail::sendToDebug($emailSubjectPrefix . "alerts cleared!", $alertMSG);
            $clearedMSG = "Alerts cleared 👍 $lb";
        }
    }
}


//------------ output to disk, then output to requester


//write email counter and lastStat
file_put_contents("lerts_emailCounter", $emailCounter);
file_put_contents("lerts_lastStat", serialize($lastStat));

//blyat it to the screen for debug?
if(!$cli)
{
	zpage::start("View 'lerts");
	?>
	<meta http-equiv="refresh" content="1; URL=?refreshPage=Y">
	<div class = "zl_cols zl_mw800">
		<div class="col"><?=zui::quip($lastStat, "Alert check results")?></div>
		<div class="col"><?=zui::quip($stats, "Processed statistics")?></div>
	</div>
    <br>
	<?php

    echo $resetTimersMSG; //let user name if statistics were reset

    //output to screen
    if($alertMSG == "")
    {
        echo $clearedMSG; //this is blank by default
        echo "All good, pardner 👍"; //yee haw
    }
    else
    {
        if($emailCounter >= $emailSet['noEmailAfterChecks'])
        {
            //zmail::sendToDebug($emailSubjectPrefix . "Resource usage alert", $alertMSG);
        }
        echo "Uh oh!$lb" . $alertMSG . "$lb";
    }
}