81 lines
5.0 KiB
PHP
81 lines
5.0 KiB
PHP
<?php
|
||||
|
|
|
|||
|
|
namespace App\Console\Commands;
|
|||
|
|
|
|||
|
|
use App\Models\SerpCheck;
|
|||
|
|
use App\Models\SerpOperationState;
|
|||
|
|
use App\Serp\Operations\ActivationManager;
|
|||
|
|
use App\Serp\Operations\CircuitBreaker;
|
|||
|
|
use App\Serp\Operations\DataForSeoHealthService;
|
|||
|
|
use App\Serp\Operations\Money;
|
|||
|
|
use Illuminate\Console\Command;
|
|||
|
|
use Illuminate\Support\Facades\Cache;
|
|||
|
|
use Illuminate\Support\Facades\DB;
|
|||
|
|
use Illuminate\Support\Facades\Schema;
|
|||
|
|
use Throwable;
|
|||
|
|
|
|||
|
|
class SerpProductionPreflightCommand extends Command
|
|||
|
|
{
|
|||
|
|
protected $signature = 'serp:production:preflight {--json}';
|
|||
|
|
|
|||
|
|
protected $description = 'Production SERP geçiş kapılarını dış HTTP olmadan doğrular';
|
|||
|
|
|
|||
|
|
public function handle(ActivationManager $activation, DataForSeoHealthService $health, CircuitBreaker $circuit): int
|
|||
|
|
{
|
|||
|
|
$gates = [];
|
|||
|
|
$gate = function (string $name, bool $pass, mixed $value = null, bool $critical = true) use (&$gates) {
|
|||
|
|
$gates[$name] = ['status' => $pass ? 'pass' : 'fail', 'critical' => $critical, 'value' => $value];
|
|||
|
|
};
|
|||
|
|
$configured = (string) config('serp.dataforseo.login') !== '' && (string) config('serp.dataforseo.password') !== '';
|
|||
|
|
$gate('environment', app()->environment('production'), app()->environment());
|
|||
|
|
$gate('debug', ! config('app.debug'), config('app.debug') ? 'enabled' : 'disabled');
|
|||
|
|
$gate('provider', config('serp.provider') === 'dataforseo', config('serp.provider'));
|
|||
|
|
$state = $activation->state();
|
|||
|
|
$consistent = (! config('serp.enabled') && in_array($state->value, ['disabled', 'validation_only', 'canary'], true)) || (config('serp.enabled') && in_array($state->value, ['canary', 'enabled', 'emergency_stopped'], true));
|
|||
|
|
$gate('activation_consistency', $consistent, ['enabled' => (bool) config('serp.enabled'), 'mode' => $state->value]);
|
|||
|
|
$gate('credentials', $configured, $configured ? 'configured' : 'not_configured');
|
|||
|
|
try {
|
|||
|
|
$local = $health->validateConfig();
|
|||
|
|
$gate('provider_security', true, ['host' => $local['host'], 'tls' => $local['tls']]);
|
|||
|
|
} catch (Throwable) {
|
|||
|
|
$gate('provider_security', false, 'invalid');
|
|||
|
|
}$pending = $this->pendingMigrations();
|
|||
|
|
$gate('migrations', $pending === 0, ['pending' => $pending]);
|
|||
|
|
try {
|
|||
|
|
$lock = Cache::lock('serp:preflight-lock', 5);
|
|||
|
|
$acquired = $lock->get();
|
|||
|
|
if ($acquired) {
|
|||
|
|
$lock->release();
|
|||
|
|
}$gate('cache_lock', $acquired, config('cache.default'));
|
|||
|
|
} catch (Throwable) {
|
|||
|
|
$gate('cache_lock', false, 'unavailable');
|
|||
|
|
}$gate('queue', in_array(config('queue.default'), ['redis', 'database'], true), ['connection' => config('queue.default'), 'serp_queue' => config('serp.queue')]);
|
|||
|
|
$console = file_get_contents(base_path('routes/console.php'));
|
|||
|
|
$gate('scheduler', str_contains($console, 'serp:dispatch-due') && str_contains($console, 'serp:reconcile'), ['defined' => true]);
|
|||
|
|
$stateRow = SerpOperationState::find(1);
|
|||
|
|
$gate('kill_switch', ! $stateRow?->emergency_stopped_at, $stateRow?->emergency_stopped_at ? 'active' : 'clear');
|
|||
|
|
$gate('circuit', ! $circuit->open(), $circuit->open() ? 'open' : 'closed');
|
|||
|
|
foreach (['daily_budget_usd', 'monthly_budget_usd', 'low_balance_usd', 'critical_balance_usd'] as $key) {
|
|||
|
|
try {
|
|||
|
|
$gate($key, Money::micros((string) config('serp.'.$key)) > 0, (string) config('serp.'.$key));
|
|||
|
|
} catch (Throwable) {
|
|||
|
|
$gate($key, false, 'invalid');
|
|||
|
|
}
|
|||
|
|
}$gate('depth', config('serp.dataforseo.result_depth') === 10, config('serp.dataforseo.result_depth'));
|
|||
|
|
$gate('provider_modes', config('serp.manual_mode') === 'live' && config('serp.automatic_mode') === 'standard', ['manual' => config('serp.manual_mode'), 'automatic' => config('serp.automatic_mode')]);
|
|||
|
|
$summary = ['status' => collect($gates)->contains(fn ($g) => $g['critical'] && $g['status'] === 'fail') ? 'failed' : 'passed', 'remote' => 'not_requested', 'gates' => $gates, 'operations' => ['last_health_at' => $stateRow?->last_health_at, 'last_provider_success_at' => $stateRow?->last_provider_success_at, 'stuck' => SerpCheck::whereIn('status', ['pending', 'running'])->where('updated_at', '<', now()->subMinutes((int) config('serp.stuck_after_minutes')))->count(), 'pending' => SerpCheck::where('status', 'pending')->count(), 'running' => SerpCheck::where('status', 'running')->count()]];
|
|||
|
|
$this->option('json') ? $this->line(json_encode($summary, JSON_THROW_ON_ERROR)) : $this->line('Preflight: '.$summary['status']);
|
|||
|
|
|
|||
|
|
return $summary['status'] === 'passed' ? self::SUCCESS : self::FAILURE;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function pendingMigrations(): int
|
|||
|
|
{
|
|||
|
|
if (! Schema::hasTable('migrations')) {
|
|||
|
|
return count(glob(database_path('migrations/*.php')));
|
|||
|
|
}$ran = DB::table('migrations')->pluck('migration')->all();
|
|||
|
|
|
|||
|
|
return count(array_filter(glob(database_path('migrations/*.php')), fn ($f) => ! in_array(pathinfo($f, PATHINFO_FILENAME), $ran, true)));
|
|||
|
|
}
|
|||
|
|
}
|