diff --git a/src/Recorders/Servers.php b/src/Recorders/Servers.php index b001a8b2..cd458087 100644 --- a/src/Recorders/Servers.php +++ b/src/Recorders/Servers.php @@ -15,6 +15,20 @@ class Servers { use Concerns\Throttling; + /** + * Callback to detect CPU usage. + * + * @var null|(callable(): int) + */ + protected static $detectCpuUsing; + + /** + * Callback to detect memory. + * + * @var null|(callable(): array{total: int, used: int}) + */ + protected static $detectMemoryUsing; + /** * The events to listen for. * @@ -32,6 +46,26 @@ public function __construct( // } + /** + * Detect CPU via the given callback. + * + * @param null|(callable(): int) $callback + */ + public static function detectCpuUsing(?callable $callback): void + { + self::$detectCpuUsing = $callback; + } + + /** + * Detect memory via the given callback. + * + * @param null|(callable(): array{total: int, used: int}) $callback + */ + public static function detectMemoryUsing(?callable $callback): void + { + self::$detectMemoryUsing = $callback; + } + /** * Record the system stats. */ @@ -41,29 +75,8 @@ public function record(SharedBeat $event): void $server = $this->config->get('pulse.recorders.'.self::class.'.server_name'); $slug = Str::slug($server); - $memoryTotal = match (PHP_OS_FAMILY) { - 'Darwin' => intval(`sysctl hw.memsize | grep -Eo '[0-9]+'` / 1024 / 1024), - 'Linux' => intval(`cat /proc/meminfo | grep MemTotal | grep -E -o '[0-9]+'` / 1024), - 'Windows' => intval(((int) trim(`wmic ComputerSystem get TotalPhysicalMemory | more +1`)) / 1024 / 1024), - 'BSD' => intval(`sysctl hw.physmem | grep -Eo '[0-9]+'` / 1024 / 1024), - default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), - }; - - $memoryUsed = match (PHP_OS_FAMILY) { - 'Darwin' => $memoryTotal - intval(intval(`vm_stat | grep 'Pages free' | grep -Eo '[0-9]+'`) * intval(`pagesize`) / 1024 / 1024), // MB - 'Linux' => $memoryTotal - intval(`cat /proc/meminfo | grep MemAvailable | grep -E -o '[0-9]+'` / 1024), // MB - 'Windows' => $memoryTotal - intval(((int) trim(`wmic OS get FreePhysicalMemory | more +1`)) / 1024), // MB - 'BSD' => intval(intval(`( sysctl vm.stats.vm.v_cache_count | grep -Eo '[0-9]+' ; sysctl vm.stats.vm.v_inactive_count | grep -Eo '[0-9]+' ; sysctl vm.stats.vm.v_active_count | grep -Eo '[0-9]+' ) | awk '{s+=$1} END {print s}'`) * intval(`pagesize`) / 1024 / 1024), // MB - default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), - }; - - $cpu = match (PHP_OS_FAMILY) { - 'Darwin' => (int) `top -l 1 | grep -E "^CPU" | tail -1 | awk '{ print $3 + $5 }'`, - 'Linux' => (int) `top -bn1 | grep -E '^(%Cpu|CPU)' | awk '{ print $2 + $4 }'`, - 'Windows' => (int) trim(`wmic cpu get loadpercentage | more +1`), - 'BSD' => (int) `top -b -d 2| grep 'CPU: ' | tail -1 | awk '{print$10}' | grep -Eo '[0-9]+\.[0-9]+' | awk '{ print 100 - $1 }'`, - default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), - }; + ['total' => $memoryTotal, 'used' => $memoryUsed] = $this->memory(); + $cpu = $this->cpu(); $this->pulse->record('cpu', $slug, $cpu, $event->time)->avg()->onlyBuckets(); $this->pulse->record('memory', $slug, $memoryUsed, $event->time)->avg()->onlyBuckets(); @@ -82,4 +95,55 @@ public function record(SharedBeat $event): void ], flags: JSON_THROW_ON_ERROR), $event->time); }); } + + /** + * CPU usage. + */ + protected function cpu(): int + { + if (self::$detectCpuUsing) { + return (self::$detectCpuUsing)(); + } + + return match (PHP_OS_FAMILY) { + 'Darwin' => (int) `top -l 1 | grep -E "^CPU" | tail -1 | awk '{ print $3 + $5 }'`, + 'Linux' => (int) `top -bn1 | grep -E '^(%Cpu|CPU)' | awk '{ print $2 + $4 }'`, + 'Windows' => (int) trim(`wmic cpu get loadpercentage | more +1`), + 'BSD' => (int) `top -b -d 2| grep 'CPU: ' | tail -1 | awk '{print$10}' | grep -Eo '[0-9]+\.[0-9]+' | awk '{ print 100 - $1 }'`, + default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), + }; + } + + /** + * Memory usage. + * + * @return array{total: int, used: int} + */ + protected function memory(): array + { + if (self::$detectMemoryUsing) { + return (self::$detectMemoryUsing)(); + } + + $memoryTotal = match (PHP_OS_FAMILY) { + 'Darwin' => intval(`sysctl hw.memsize | grep -Eo '[0-9]+'` / 1024 / 1024), + 'Linux' => intval(`cat /proc/meminfo | grep MemTotal | grep -E -o '[0-9]+'` / 1024), + 'Windows' => intval(((int) trim(`wmic ComputerSystem get TotalPhysicalMemory | more +1`)) / 1024 / 1024), + 'BSD' => intval(`sysctl hw.physmem | grep -Eo '[0-9]+'` / 1024 / 1024), + default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), + }; + + $memoryUsed = match (PHP_OS_FAMILY) { + 'Darwin' => $memoryTotal - intval(intval(`vm_stat | grep 'Pages free' | grep -Eo '[0-9]+'`) * intval(`pagesize`) / 1024 / 1024), // MB + 'Linux' => $memoryTotal - intval(`cat /proc/meminfo | grep MemAvailable | grep -E -o '[0-9]+'` / 1024), // MB + 'Windows' => $memoryTotal - intval(((int) trim(`wmic OS get FreePhysicalMemory | more +1`)) / 1024), // MB + 'BSD' => intval(intval(`( sysctl vm.stats.vm.v_cache_count | grep -Eo '[0-9]+' ; sysctl vm.stats.vm.v_inactive_count | grep -Eo '[0-9]+' ; sysctl vm.stats.vm.v_active_count | grep -Eo '[0-9]+' ) | awk '{s+=$1} END {print s}'`) * intval(`pagesize`) / 1024 / 1024), // MB + default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), + }; + + return [ + 'total' => $memoryTotal, + 'used' => $memoryUsed, + ]; + } } diff --git a/tests/Feature/Recorders/ServersTest.php b/tests/Feature/Recorders/ServersTest.php index 8490df98..02962fd5 100644 --- a/tests/Feature/Recorders/ServersTest.php +++ b/tests/Feature/Recorders/ServersTest.php @@ -34,3 +34,31 @@ expect($aggregates->pluck('key')->unique()->values()->all())->toBe(['foo']); expect($aggregates->pluck('aggregate')->unique()->values()->all())->toBe(['avg']); }); + +it('can customise CPU and memory resolution', function () { + Config::set('pulse.recorders.'.Servers::class.'.server_name', 'Foo'); + Date::setTestNow(Date::now()->startOfMinute()); + + Servers::detectCpuUsing(fn () => 987654321); + Servers::detectMemoryUsing(fn () => [ + 'total' => 123456789, + 'used' => 1234, + ]); + event(new SharedBeat(CarbonImmutable::now(), 'instance-id')); + Pulse::ingest(); + + $value = Pulse::ignore(fn () => DB::table('pulse_values')->sole()); + + $payload = json_decode($value->value); + expect($payload->cpu)->toBe(987654321); + expect($payload->memory_used)->toBe(1234); + expect($payload->memory_total)->toBe(123456789); + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->get()); + expect($aggregates->count())->toBe(8); + expect($aggregates->pluck('type')->unique()->values()->all())->toBe(['cpu', 'memory']); + expect($aggregates->pluck('value')->unique()->values()->all())->toEqual(['987654321.00', '1234.00']); + + Servers::detectCpuUsing(null); + Servers::detectMemoryUsing(null); +});