Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

System Metrics Laravel Package

cboxdk/system-metrics

Get real-time system metrics from Linux and macOS in pure PHP. No extensions or dependencies—just type-safe, readonly classes covering CPU, memory, storage, network, load, uptime, and container metrics with explicit Result-based error handling.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and check the platform gate first. The package requires PHP 8.3+ and Linux/macOS only — no Windows. In a Laravel app, run composer require cboxdk/system-metrics and immediately confirm your production and CI images satisfy the version floor, because composer install will fail on a 8.2 host that otherwise runs your app fine. Windows developers need Sail, Docker, or WSL; php artisan serve on a Windows host will not work.

composer require cboxdk/system-metrics
php -r "require 'vendor/autoload.php'; var_dump(Cbox\SystemMetrics\SystemMetrics::memory()->isSuccess());"

There is no Laravel service provider, no config file, and no facade alias to publish. It is a framework-agnostic library with a static entry point. Nothing appears under config/, and php artisan vendor:publish will not surface anything. Plan for that: you will wrap it yourself rather than configuring it.

Where to look first, in order:

  1. docs/quickstart.md and docs/api-reference.md — the surface is wide (environment, CPU, memory, load, uptime, storage, network, container, limits, process) and you only need three or four of these.
  2. docs/architecture/result-pattern.md — every call returns Result<T>, never throws. This changes how you write every consumer.
  3. docs/platform-support/comparison.md — the macOS and Linux feature matrices differ; dev-on-Mac/prod-on-Linux is the common Laravel setup and you need to know which fields go null where.
  4. changelog/v3.0.0.md — a recent breaking release flipped CPU core fields from int to float and uncapped utilisation percentages. Anything you copy from a blog post or an LLM is likely v2-era code.

First use case: a scheduled snapshot behind a /health/system route. Do not call this from middleware or a controller on every request. Compute a snapshot once a minute in the scheduler and serve it from cache.

// app/Console/Commands/SystemSnapshot.php
namespace App\Console\Commands;

use Cbox\SystemMetrics\SystemMetrics;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;

class SystemSnapshot extends Command
{
    protected $signature = 'metrics:snapshot';

    public function handle(): int
    {
        $result = SystemMetrics::overview();

        if (! $result->isSuccess()) {
            $this->error($result->getError()->getMessage());
            Cache::forget('system:snapshot');   // stale data is worse than none
            return self::FAILURE;
        }

        $o = $result->getValue();

        Cache::put('system:snapshot', [
            'os'         => $o->environment->os->name.' '.$o->environment->os->version,
            'cores'      => $o->cpu->coreCount(),
            'memory_pct' => round($o->memory->usedPercentage(), 1),
            'load_1m'    => $o->loadAverage?->oneMinute,
            'uptime'     => $o->uptime?->humanReadable(),
            'container'  => $o->limits?->isContainerized() ?? false,
            'mem_limit'  => $o->limits ? round($o->limits->memoryUtilization(), 1) : null,
        ], now()->addMinutes(2));

        return self::SUCCESS;
    }
}
// routes/console.php (Laravel 11+) or app/Console/Kernel.php
Schedule::command('metrics:snapshot')->everyMinute()->withoutOverlapping();

Route the read side off the cache, and treat a cold cache as "unknown", not "unhealthy", so a scheduler hiccup doesn't page anyone.

Implementation Patterns

Wrap the static facade in your own container-bound service

The static entry point is the single biggest ergonomic cost in Laravel. You cannot inject SystemMetrics, so tests hit real /proc and sysctl. Put a thin interface in front of it and bind it — everything downstream becomes mockable and swappable.

// app/Contracts/SystemProbe.php
interface SystemProbe
{
    public function usedMemoryPercentage(): ?float;
    public function loadPerCore(): ?float;
    public function availableCpuCores(): ?float;
    public function isContainerized(): bool;
}
// app/Services/SystemMetricsProbe.php
use Cbox\SystemMetrics\SystemMetrics;

class SystemMetricsProbe implements SystemProbe
{
    public function usedMemoryPercentage(): ?float
    {
        $r = SystemMetrics::memory();
        return $r->isSuccess() ? $r->getValue()->usedPercentage() : null;
    }

    public function loadPerCore(): ?float
    {
        $r = SystemMetrics::loadAverage();
        return $r->isSuccess() ? $r->getValue()->perCoreNormalized() : null;
    }

    public function availableCpuCores(): ?float
    {
        $r = SystemMetrics::limits();
        return $r->isSuccess() ? $r->getValue()->availableCpuCores() : null;
    }

    public function isContainerized(): bool
    {
        $r = SystemMetrics::environment();
        return $r->isSuccess() && ($r->getValue()->insideContainer ?? false);
    }
}

Bind it in a SystemMetricsServiceProvider. Return null on failure rather than propagating Result — your domain code should not import a metrics library's types.

Load-shedding and graceful degradation

This is the pattern that actually pays for the dependency day-to-day: reading load and memory is cheap, and it lets you turn off expensive optional work before the box falls over.

class DegradeHeavyFeatures
{
    public function __construct(private SystemProbe $probe) {}

    public function handle($request, Closure $next)
    {
        $load = $this->probe->loadPerCore();

        if ($load !== null && $load > 1.5) {
            config(['features.exports' => false, 'features.thumbnails' => false]);
            Log::warning('feature.degraded', ['load_per_core' => $load]);
        }

        return $next($request);
    }
}

Keep the threshold on perCoreNormalized-style values, not raw load average — a raw 4.0 means something completely different on 2 cores than on 32. If the package's normalisation method isn't available in your version, divide oneMinute by $cpu->coreCount() yourself.

Sizing queue workers from cgroup limits

In Kubernetes or Docker with an 200m/1500m CPU quota, the host core count is a lie. SystemMetrics::limits() gives you the fractional quota actually applied to the container — exactly what you want when provisioning Horizon or a supervisor worker count at deploy time.

// a provisioning command or entrypoint script
$result = SystemMetrics::limits();
$cores = $result->isSuccess() ? $result->getValue()->availableCpuCores() : 2.0;

$this->line('Suggested workers: ' . max(1, (int) floor($cores * 2)));

Same for memory: availableMemoryBytes() vs memory_get_usage(true) is the honest comparison inside a container, whereas memory_limit in php.ini may still say -1 or a host-sized number.

Disk and log-growth watchdog

SystemMetrics::storage() is the practical use for the storage reader in a Laravel app — not capacity planning, but catching the day /var/log or storage/logs fills the volume.

$storage = SystemMetrics::storage()->getValue();
if ($storage->usedPercentage() > 85) {
    Notification::route('slack', config('services.slack.ops'))->notify(new DiskFillingUp($storage));
}

Run it hourly in the scheduler, not per request. Note it reports filesystem-level usage, so on a container the root overlay often reflects the mount, not the writable layer — verify the number against df -h on your actual image before trusting the threshold.

Per-job process memory telemetry

Inside a queued job, ProcessMetrics::snapshot(getmypid()) gives you RSS for that worker, which is far more actionable than memory_get_peak_usage() when a job leaks through an unbounded collection or a large query.

public function handle(): void
{
    $proc = ProcessMetrics::snapshot(getmypid())->getValue();
    $rssMb = round($proc->resources->memoryRssBytes / 1024 ** 2, 2);

    Log::withContext(['rss_mb' => $rssMb, 'job' => static::class]);
    // ...
}

Wrap it in a base job class so you get the number on every job's log line for free.

CPU percentage without blocking a request

SystemMetrics::cpuUsage(1.0) sleeps for a full second. That belongs in a scheduled command or a worker-side sampler, never in a controller, middleware, or Livewire action. For a dashboard, prefer the two-snapshot form so the sleep is decoupled from the request:

// metrics:snapshot --cpu
$before = SystemMetrics::cpu()->getValue();
sleep(2);
$after  = SystemMetrics::cpu()->getValue();
$delta  = CpuSnapshot::calculateDelta($before, $after);

Cache::put('system:cpu', [
    'total'   => round($delta->usagePercentage(), 1),
    'user'    => round($delta->userPercentage(), 1),
    'system'  => round($delta->systemPercentage(), 1),
    'iowait'  => round($delta->iowaitPercentage(), 1),
    'busiest' => $delta->busiestCore(),
], now()->addMinute());

iowaitPercentage() and busiestCore() are the two fields that actually explain "why is the app slow" — a 90% iowait reading points at disk, not PHP.

Health checks via an existing package

If you use spatie/laravel-health or Laravel Pulse, consume this package inside a custom check rather than building a bespoke endpoint. Return failed() when memory or disk crosses a threshold, ok() otherwise, and let the health package own the route, caching, and notification plumbing.

class SystemResourcesCheck extends Check
{
    public function run(): Result
    {
        $mem = SystemMetrics::memory();
        if (! $mem->isSuccess()) {
            return Result::failed('Memory metrics unavailable');
        }

        $pct = $mem->getValue()->usedPercentage();

        return $pct > 92
            ? Result::failed("Memory at {$pct}%")
            : Result::ok("Memory at {$pct}%");
    }
}

Log context middleware (with restraint)

Adding load and memory to Log::withContext once per request is genuinely useful when triaging an incident after the fact. Do it in a terminable middleware so it runs after the response, and accept that on Octane the values may be stale (see Gotchas).

Gotchas and Tips

Result<T> never throws, so nothing lands in your exception handler. Failures are silent unless you check. Use the fluent form for the low-ceremony case, and never call getValue() before confirming isSuccess() — the README's terse ->getValue() examples assume success:

SystemMetrics::cpu()
    ->onSuccess(fn ($cpu) => Cache::put('cores', $cpu->coreCount(), 3600))
    ->onFailure(fn ($err) => Log::debug('metrics.cpu.failed', ['error' => $err->getMessage()]));

Treat a failure as "metrics unknown", not "system broken". Every reader is filesystem- or shell-based and can legitimately fail inside hardened containers.

Windows is a hard no. No proc fallback, no stub. If any developer or CI runner is on Windows outside a container, the package will fail at runtime even after a successful install.

macOS reads via sysctl, vm_stat, sw_vers. If disable_functions blocks exec/shell_exec/proc_open — common on shared hosting and some hardened PHP images — the macOS path dies silently into a Result failure. On Linux it's /proc//sys reads, so open_basedir restrictions are the equivalent trap.

Static data caching plus Octane equals stale metrics. The docs advertise internal caching of static data (OS, architecture, core count). Under RoadRunner/Swoole/Octane those statics live for the worker's lifetime, so a container that gets resized from 500m to 2000m may keep reporting the old quota until workers recycle. If you use Octane, resolve the probe per-request or add a php artisan octane:reload to your resize runbook.

overview() is all-or-nothing. One failed sub-reader likely fails the whole composite. For a health dashboard where you'd rather show three of five panels, call the individual readers and degrade each independently. Use overview() only where you genuinely need a coherent single timestamp.

Nullable fields everywhere. loadAverage, uptime, and limits can be null in the overview. The README already nullsafe-chains them ($o->loadAverage?->oneMinute); follow that, and type-hint your own extractors as ?float / ?string. PHPStan level 9 in this package means your static analysis will correctly flag every unguarded access — let it.

v3.0.0 floats will bite copied code. cpuCores, currentCpuCores, and availableCpuCores() are float now, and cpuUtilizationPercentage() / memoryUtilizationPercentage() are no longer capped at 100% — over-quota bursts legitimately read 340%. Any int type hint, (int) cast, min(100, ...) assumption, or percentage-to-progress-bar component needs a min(100.0, ...) at the presentation layer. Pin the version explicitly in composer.json rather than floating on ^2 or ^3 until you've read the migration guide.

Don't sample per request. Reading /proc is cheap but not free, and the CPU-percentage path blocks for a second. Per-request sampling under load adds precisely the load you're measuring. One scheduled snapshot per minute into cache/Redis is the correct cadence; serve dashboards and health endpoints from that.

No injection point for config. There's no config/system-metrics.php, so thresholds, cache keys, and TTLs live in your own config file. Do this deliberately — it's the thing you'll want to tune per environment, and it makes the wrapper service testable.

Low adoption, single maintainer, 0 dependents. Seven stars and no downstream consumers means you are the de facto QA. Mitigations: wrap it behind your own interface (as above) so swapping to php-osx-style shell calls or a Prometheus sidecar is a one-file change; pin an exact version; and add a contract test asserting the fields you depend on still have the shapes you expect after any upgrade.

Testing on CI. GitHub Actions Linux runners expose /proc fine, but the numbers are shared-runner noise — assert on shape (non-null, sane range), not values. If the package ships fakes (the changelog references FakeSystemMetricsTest), use them for unit tests and reserve real reads for a single smoke test. Never assert a specific core count.

Prefer limits over memory in containers. SystemMetrics::memory() reflects the host's physical RAM; SystemMetrics::limits()->memoryUtilization() is cgroup-aware and reflects what your container can actually use. In Kubernetes these diverge by an order of magnitude, and using the wrong one makes your alerts meaningless.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen