Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

分享一个简单的 laravel 应用健康检查命令 #45

Open
guanguans opened this issue Jun 22, 2022 · 0 comments
Open

分享一个简单的 laravel 应用健康检查命令 #45

guanguans opened this issue Jun 22, 2022 · 0 comments
Labels
2022 2022 Laravel Laravel PHP PHP

Comments

@guanguans
Copy link
Owner

guanguans commented Jun 22, 2022

分享一个简单的 laravel 应用健康检查命令

分享一个简单的 laravel 应用健康检查命令(HealthCheckCommand)。

效果

health-check

HealthCheckCommand

<?php

namespace App\Console\Commands;

use App\Enums\HealthCheckStateEnum;
use DateTime;
use DateTimeZone;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use ReflectionMethod;
use ReflectionObject;
use Throwable;

class HealthCheckCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'health:check';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Health check.';

    /**
     * @var array
     */
    protected $except = [
        '*Queue'
    ];

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        collect((new ReflectionObject($this))->getMethods(ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE))
            ->filter(function (ReflectionMethod $method) {
                return (bool)(string)Str::of($method->name)->pipe(function (Stringable $name) {
                    return $name->startsWith('check') && ! $name->is($this->except);
                });
            })
            ->sortBy(function (ReflectionMethod $method) {
                return $method->name;
            })
            ->pipe(function (Collection $methods) {
                $this->withProgressBar($methods, function ($method) use (&$checks) {
                    /* @var HealthCheckStateEnum $state */
                    $state = call_user_func([$this, $method->name]);

                    $checks[] = [
                        'index' => count((array)$checks) + 1,
                        'resource' => Str::of($method->name)->replaceFirst('check', ''),
                        'state' => $state,
                        'message' => $state->description,
                    ];
                });

                $this->newLine();
                $this->table(['Index', 'Resource', 'State', 'Message'], $checks);

                return collect($checks);
            })
            ->tap(function (Collection $checks) {
                $checks
                    ->filter(function ($check) {
                        return $check['state']->isNot(HealthCheckStateEnum::OK);
                    })
                    ->whenNotEmpty(function (Collection $notOkChecks) {
                        // event(new HealthCheckFailedEvent($notOkChecks));
                        $this->error('Health check failed.');

                        return $notOkChecks;
                    })
                    ->whenEmpty(function (Collection $notOkChecks) {
                        // event(new HealthCheckPassedEvent());
                        $this->info('Health check passed.');

                        return $notOkChecks;
                    });
            });

        return 0;
    }

    /**
     * @param $connection
     *
     * @return \App\Enums\HealthCheckStateEnum
     */
    protected function checkDatabase($connection = null): HealthCheckStateEnum
    {
        try {
            DB::connection($connection ?: config('database.default'))->getPdo();
        } catch (Throwable $e) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($e) {
                $state->description = "Could not connect to the database: `{$e->getMessage()}`";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    /**
     * @return \App\Enums\HealthCheckStateEnum
     */
    protected function checkSqlSafeUpdates(): HealthCheckStateEnum
    {
        if (config('database.default') !== 'mysql') {
            return tap(HealthCheckStateEnum::WARNING(), function (HealthCheckStateEnum $state) {
                $state->description = 'This check is only available for MySQL.';
            });
        }

        $sqlSafeUpdates = DB::select("SHOW VARIABLES LIKE 'sql_safe_updates' ")[0];
        if (! Str::of($sqlSafeUpdates->Value)->lower()->is('on')) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) {
                $state->description = '`sql_safe_updates` is disabled. Please enable it.';
            });
        }

        return HealthCheckStateEnum::OK();
    }

    /**
     * @param  array|string  $checkedSqlModes
     *
     * @return \App\Enums\HealthCheckStateEnum
     */
    protected function checkSqlMode($checkedSqlModes = 'strict_all_tables'): HealthCheckStateEnum
    {
        if (config('database.default') !== 'mysql') {
            return tap(HealthCheckStateEnum::WARNING(), function (HealthCheckStateEnum $state) {
                $state->description = 'This check is only available for MySQL.';
            });
        }

        $sqlModes = DB::select("SHOW VARIABLES LIKE 'sql_mode' ")[0];

        /* @var Collection $diffSqlModes */
        $diffSqlModes = Str::of($sqlModes->Value)
            ->lower()
            ->explode(',')
            ->pipe(function (Collection $sqlModes) use ($checkedSqlModes): Collection {
                return collect($checkedSqlModes)
                    ->transform(function (string $checkedSqlMode) {
                        return Str::of($checkedSqlMode)->lower();
                    })
                    ->diff($sqlModes);
            });
        if ($diffSqlModes->isNotEmpty()) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($diffSqlModes) {
                $state->description = "`sql_mode` is not set to `{$diffSqlModes->implode('、')}`. Please set to them.";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    /**
     * @return \App\Enums\HealthCheckStateEnum
     * @throws \Exception
     */
    protected function checkTimeZone(): HealthCheckStateEnum
    {
        if (config('database.default') !== 'mysql') {
            return tap(HealthCheckStateEnum::WARNING(), function (HealthCheckStateEnum $state) {
                $state->description = 'This check is only available for MySQL.';
            });
        }

        $dbTimeZone = DB::select("SHOW VARIABLES LIKE 'time_zone' ")[0]->Value;
        Str::of($dbTimeZone)->lower()->is('system') and $dbTimeZone = DB::select("SHOW VARIABLES LIKE 'system_time_zone' ")[0]->Value;

        $dbDateTime = (new DateTime('now', new DateTimeZone($dbTimeZone)))->format('YmdH');
        $appDateTime = (new DateTime('now', new DateTimeZone($appTimezone = config('app.timezone'))))->format('YmdH');
        if ($dbDateTime !== $appDateTime) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($appTimezone, $dbTimeZone) {
                $state->description = "The database timezone(`$dbTimeZone`) is not equal to app timezone(`$appTimezone`).";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    /**
     * @param  null|string  $url
     *
     * @return \App\Enums\HealthCheckStateEnum
     */
    protected function checkPing(?string $url = null): HealthCheckStateEnum
    {
        $url = $url ?: config('app.url');

        $response = Http::get($url);
        if ($response->failed()) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($response) {
                $state->description = "Could not connect to the application: `{$response->body()}`";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    /**
     * @return \App\Enums\HealthCheckStateEnum
     */
    protected function checkPhpVersion(): HealthCheckStateEnum
    {
        if (version_compare(PHP_VERSION, '7.3.0', '<')) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) {
                $state->description = 'PHP version is less than 7.3.0.';
            });
        }

        return HealthCheckStateEnum::OK();
    }

    protected function checkPhpExtensions(): HealthCheckStateEnum
    {
        $extensions = [
            'curl',
            'gd',
            'mbstring',
            'openssl',
            'pdo',
            'pdo_mysql',
            'xml',
            'zip',
            'swoole',
        ];

        /* @var Collection $missingExtensions */
        $missingExtensions = collect($extensions)
            ->reduce(function (Collection $missingExtensions, $extension) {
                return $missingExtensions->when(! extension_loaded($extension), function (Collection $missingExtensions) use ($extension) {
                    return $missingExtensions->add($extension);
                });
            }, collect());

        if ($missingExtensions->isNotEmpty()) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($missingExtensions) {
                $state->description = "The following PHP extensions are missing: `{$missingExtensions->implode('、')}`.";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    /**
     * @return \App\Enums\HealthCheckStateEnum
     */
    protected function checkDiskSpace(): HealthCheckStateEnum
    {
        $freeSpace = disk_free_space(base_path());
        $diskSpace = sprintf('%.1f', $freeSpace / (1024 * 1024));
        if ($diskSpace < 100) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($diskSpace) {
                $state->description = "The disk space is less than 100MB: `$diskSpace`.";
            });
        }

        $diskSpace = sprintf('%.1f', $freeSpace / (1024 * 1024 * 1024));
        if ($diskSpace < 1) {
            return tap(HealthCheckStateEnum::WARNING(), function (HealthCheckStateEnum $state) use ($diskSpace) {
                $state->description = "The disk space is less than 1GB: `$diskSpace`.";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    protected function checkMemoryLimit(int $limit = 128): HealthCheckStateEnum
    {
        $inis = collect(ini_get_all())->filter(function ($value, $key) {
            return str_contains($key, 'memory_limit');
        });

        if ($inis->isEmpty()) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) {
                $state->description = "The memory limit is not set.";
            });
        }

        $localValue = $inis->first()['local_value'];
        if ($localValue < $limit) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) use ($limit, $localValue) {
                $state->description = "The memory limit is less than {$limit}M: `$localValue`.";
            });
        }

        return HealthCheckStateEnum::OK();
    }

    protected function checkQueue(): HealthCheckStateEnum
    {
        if (! Queue::connected()) {
            return tap(HealthCheckStateEnum::FAILING(), function (HealthCheckStateEnum $state) {
                $state->description = "The queue is not connected.";
            });
        }

        return HealthCheckStateEnum::OK();
    }
}

HealthCheckStateEnum

<?php

namespace App\Enums;

use BenSampo\Enum\Enum;

/**
 * @method static static OK()
 * @method static static WARNING()
 * @method static static FAILING()
 */
final class HealthCheckStateEnum extends Enum
{
    public const OK = '<info>ok</info>';
    public const WARNING = '<comment>warning</comment>';
    public const FAILING = '<error>failing</error>';
}

原文链接

@guanguans guanguans added PHP PHP Laravel Laravel labels Jun 22, 2022
@guanguans guanguans added the 2022 2022 label Nov 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2022 2022 Laravel Laravel PHP PHP
Projects
None yet
Development

No branches or pull requests

1 participant