Docker-PHP/swarm-monitor/agent
2022-06-27 13:25:23 +02:00

379 lines
11 KiB
PHP
Executable file

#!/usr/bin/env php
<?php
require_once 'vendor/autoload.php';
use Bramus\Monolog\Formatter\ColoredLineFormatter;
use GuzzleHttp\Client as Guzzle;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Spatie\Emoji\Emoji;
class AgentState
{
private string $hostname = 'unknown host';
/** @var string[] */
private array $labels = [];
private int $containerCount = 0;
private int $containerCountRunning = 0;
private int $memAvailableKB = 0;
private int $memFreeKB = 0;
private int $memTotalKB = 0;
private int $swapFreeKB = 0;
private int $swapTotalKB = 0;
public function __toString(): string
{
return sprintf(
'Population: %d running of %d. Memory %.2fGB/%.2fGB. Swap %.2fGB/%.2fGB',
$this->getContainerCountRunning(),
$this->getContainerCount(),
$this->getMemAvailableKB() / 1024 / 1024,
$this->getMemTotalKB() / 1024 / 1024,
$this->getSwapFreeKB() / 1024 / 1024,
$this->getSwapTotalKB() / 1024 / 1024,
);
}
public function emit(Redis $redis, int $redisExpiry = 60): void
{
$instanceDataKey = "swarm:{$this->getHostname()}";
$redis->hset($instanceDataKey, "population_running", $this->getContainerCountRunning());
$redis->hset($instanceDataKey, "population_total", $this->getContainerCount());
$redis->hset($instanceDataKey, "memory_available_kb", $this->getMemAvailableKB());
$redis->hset($instanceDataKey, "memory_free_kb", $this->getMemFreeKB());
$redis->hset($instanceDataKey, "memory_total_kb", $this->getMemTotalKB());
$redis->hset($instanceDataKey, "swap_free_kb", $this->getSwapFreeKB());
$redis->hset($instanceDataKey, "swap_total_kb", $this->getSwapTotalKB());
$redis->hset($instanceDataKey, "updated_at", date("Y-m-d H:i:s"));
$redis->hset($instanceDataKey, "labels", implode(", ", $this->getLabels()));
$redis->expire($instanceDataKey, $redisExpiry);
}
public function getHostname(): string
{
return strtolower($this->hostname);
}
public function setHostname(string $hostname): AgentState
{
$this->hostname = $hostname;
return $this;
}
public function getContainerCount(): int
{
return $this->containerCount;
}
public function setContainerCount(int $containerCount): AgentState
{
$this->containerCount = $containerCount;
return $this;
}
public function getContainerCountRunning(): int
{
return $this->containerCountRunning;
}
public function setContainerCountRunning(int $containerCountRunning): AgentState
{
$this->containerCountRunning = $containerCountRunning;
return $this;
}
public function getMemAvailableKB(): int
{
return $this->memAvailableKB;
}
public function setMemAvailableKB(int $memAvailableKB): AgentState
{
$this->memAvailableKB = $memAvailableKB;
return $this;
}
public function getMemFreeKB(): int
{
return $this->memFreeKB;
}
public function setMemFreeKB(int $memFreeKB): AgentState
{
$this->memFreeKB = $memFreeKB;
return $this;
}
public function getMemTotalKB(): int
{
return $this->memTotalKB;
}
public function setMemTotalKB(int $memTotalKB): AgentState
{
$this->memTotalKB = $memTotalKB;
return $this;
}
public function getSwapFreeKB(): int
{
return $this->swapFreeKB;
}
public function setSwapFreeKB(int $swapFreeKB): AgentState
{
$this->swapFreeKB = $swapFreeKB;
return $this;
}
public function getSwapTotalKB(): int
{
return $this->swapTotalKB;
}
public function setSwapTotalKB(int $swapTotalKB): AgentState
{
$this->swapTotalKB = $swapTotalKB;
return $this;
}
/**
* @return string[]
*/
public function getLabels(): array
{
return $this->labels;
}
/**
* @param string[] $labels
* @return AgentState
*/
public function setLabels(array $labels): AgentState
{
$this->labels = $labels;
return $this;
}
}
class Agent
{
private array $environment;
private Guzzle $client;
private Logger $logger;
private ?string $instanceStateHash = null;
private ?AgentState $currentState = null;
private ?AgentState $lastState = null;
private Redis $redis;
private int $minimumUpdateIntervalSeconds = 60;
private int $lastRunTimestamp = 0;
public function __construct()
{
$this->environment = array_merge($_ENV, $_SERVER);
ksort($this->environment);
$this->logger = new Monolog\Logger('agent');
$this->logger->pushHandler(new StreamHandler('/var/log/swarm-agent.log', Logger::DEBUG));
$stdout = new StreamHandler('php://stdout', Logger::DEBUG);
$stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n"));
$this->logger->pushHandler($stdout);
if(isset($this->environment['MINIMUM_UPDATE_INTERVAL'])){
$this->minimumUpdateIntervalSeconds = $this->environment['MINIMUM_UPDATE_INTERVAL'];
}
$this->redis = new Redis();
$this->redis->pconnect(
$this->environment['REDIS_HOST'] ?? "redis",
$this->environment['REDIS_PORT'] ?? 6379
);
$this->client = new Guzzle(
[
'base_uri' => 'http://localhost',
'curl' => [
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
],
]
);
// Prevent double-starting
$this->lastRunTimestamp = time();
}
public function findContainersSwarmMode(): array
{
$services = json_decode($this->client->request('GET', 'services')->getBody()->getContents(), true);
}
public function run(): void
{
$this->logger->info(sprintf('%s Starting Swarm Agent...', Emoji::CHARACTER_TIMER_CLOCK));
while (true) {
$this->runLoop();
}
}
private function calculateStateHash(): string
{
$newInstanceStates = [];
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
foreach ($containers as $container) {
$inspect = json_decode($this->client->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
$newInstanceStates['container-'.$inspect['Id']] = implode('::', [
$inspect['Name'],
$inspect['Created'],
$inspect['Image'],
$inspect['State']['Status'],
sha1(implode('|', $inspect['Config']['Env'])),
]);
}
return sha1(implode("\n", $newInstanceStates));
}
/**
* Returns true when something has changed.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
private function stateHasChanged(): bool
{
$newStateHash = $this->calculateStateHash();
// $this->logger->debug(sprintf("Old state = %s. New State = %s.", substr($this->instanceStateHash,0,7), substr($newStateHash, 0,7)));
if (!$this->instanceStateHash || $this->instanceStateHash != $newStateHash) {
$this->instanceStateHash = $newStateHash;
return true;
}
return false;
}
private function timeElapsed() : bool
{
if($this->lastRunTimestamp < time() - $this->minimumUpdateIntervalSeconds){
$this->lastRunTimestamp = time();
$this->logger->debug(sprintf("Max interval of %d seconds has elapsed", $this->minimumUpdateIntervalSeconds));
return true;
}
return false;
}
private function containerInventory(): void
{
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
$runningContainers = 0;
foreach ($containers as $container) {
if ($container['State'] == 'running') {
++$runningContainers;
} else {
\Kint::dump($container['State']);
exit;
}
}
$this->currentState
->setContainerCount(count($containers))
->setContainerCountRunning($runningContainers)
;
}
private function runLoop(): void
{
if ($this->currentState instanceof AgentState) {
$this->lastState = $this->currentState;
}
$this->currentState = (new AgentState());
$this->probeSysInfo();
$this->containerInventory();
$this->probeMemory();
$this->probeDisks();
$this->logger->debug($this->currentState->__toString());
$this->currentState->emit($this->redis, $this->minimumUpdateIntervalSeconds+30);
$this->waitUntilContainerChange();
}
private function probeSysInfo() : void
{
$info = json_decode($this->client->request('GET', 'info')->getBody()->getContents(), true);
$this->currentState->setHostname($info['Name']);
$this->currentState->setLabels($info['Labels']);
}
private function probeMemory(): void
{
$memInfo = file_get_contents('/proc/meminfo');
foreach (explode("\n", $memInfo) as $line) {
if (stripos($line, ':') === false) {
continue;
}
[$key, $value] = explode(':', $line);
$key = trim($key);
$value = trim($value);
$value = str_replace(' kB', '', $value);
switch ($key) {
case 'MemTotal':
$this->currentState->setMemTotalKB($value);
break;
case 'MemFree':
$this->currentState->setMemFreeKB($value);
break;
case 'MemAvailable':
$this->currentState->setMemAvailableKB($value);
break;
case 'SwapTotal':
$this->currentState->setSwapTotalKB($value);
break;
case 'SwapFree':
$this->currentState->setSwapFreeKB($value);
break;
}
}
}
private function probeDisks(): void
{
// @todo
// $diskinfo = file_get_contents("/proc/diskstats")
}
private function waitUntilContainerChange(): void
{
while ($this->stateHasChanged() === false && $this->timeElapsed() === false) {
sleep(5);
}
$this->logger->info(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING));
}
}
(new Agent())->run();