Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 130 |
| CommandController | |
0.00% |
0 / 1 |
|
0.00% |
0 / 13 |
1260 | |
0.00% |
0 / 130 |
| index | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
| store | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
| show | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
| update | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
| destroy | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 2 |
|||
| validateData | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 9 |
|||
| stopCommands | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 35 |
|||
| getArtisanCommands | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 17 |
|||
| execCommand | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
|||
| getSignatures | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 15 |
|||
| getSignature | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
| getMonitoringHistory | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 23 |
|||
| serviceScaleUp | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
| 1 | <?php |
| 2 | |
| 3 | namespace Qmp\Laravel\ApiGateway\Controllers\Monitoring; |
| 4 | |
| 5 | use Illuminate\Database\Eloquent\Collection as EloquentCollection; |
| 6 | use Illuminate\Http\Request; |
| 7 | use Illuminate\Http\JsonResponse; |
| 8 | use Illuminate\Http\Response; |
| 9 | use Illuminate\Support\Carbon; |
| 10 | use Illuminate\Support\Facades\Artisan; |
| 11 | use Illuminate\Support\Collection; |
| 12 | use Illuminate\Support\Facades\Cache; |
| 13 | use Illuminate\Support\Facades\DB; |
| 14 | use Illuminate\Support\Facades\Log; |
| 15 | use Illuminate\Support\Str; |
| 16 | use Illuminate\Validation\Rule; |
| 17 | use Qmp\Laravel\ApiGateway\Controllers\AbstractApiController; |
| 18 | use Qmp\Laravel\CommandsLaravel\Models\CommandMonitoring; |
| 19 | use Qmp\Laravel\CommandsLaravel\Models\Cron; |
| 20 | use Qmp\Laravel\MicroService\Client\Client; |
| 21 | use Qmp\Laravel\MicroService\Client\Tools\Request as ClientRequest; |
| 22 | use ReflectionException; |
| 23 | |
| 24 | class CommandController extends AbstractApiController |
| 25 | { |
| 26 | const CACHE_KEY_SIGNATURES = 'artisan-commands-signatures'; |
| 27 | const SUBDAYS_MONITORING = 1; |
| 28 | |
| 29 | /** |
| 30 | * @return EloquentCollection|Cron[] |
| 31 | */ |
| 32 | public function index(): EloquentCollection |
| 33 | { |
| 34 | return Cron::all(); |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Store a newly created resource in storage. |
| 39 | * |
| 40 | * @param Request $request |
| 41 | * @return JsonResponse |
| 42 | */ |
| 43 | public function store(Request $request): JsonResponse |
| 44 | { |
| 45 | |
| 46 | $validatedData = array_merge($this->validateData($request), ['prod' => env('PROD')]); |
| 47 | Cron::create($validatedData); |
| 48 | |
| 49 | return response()->json(['status' => 'ok']); |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Display the specified resource. |
| 54 | * |
| 55 | * @param Cron |
| 56 | * @return JsonResponse |
| 57 | */ |
| 58 | public function show(Cron $cron): JsonResponse |
| 59 | { |
| 60 | return response()->json([ |
| 61 | 'cron' => $cron->toArray() |
| 62 | ]); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Update the specified resource in storage. |
| 67 | * |
| 68 | * @param Request $request |
| 69 | * @param $id |
| 70 | * @return JsonResponse |
| 71 | */ |
| 72 | public function update(Request $request, $id): JsonResponse |
| 73 | { |
| 74 | |
| 75 | $validatedData = array_merge($this->validateData($request, $id), ['prod' => env('PROD')]); |
| 76 | |
| 77 | $model = Cron::findOrFail($id); |
| 78 | $result = $model->update($validatedData); |
| 79 | |
| 80 | return response()->json(['status' => $result ? 'ok' : 'ko']); |
| 81 | |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Remove the specified resource from storage. |
| 86 | * |
| 87 | * @param $id |
| 88 | * @return JsonResponse |
| 89 | */ |
| 90 | public function destroy(Cron $cron): JsonResponse |
| 91 | { |
| 92 | $result = $cron->delete(); |
| 93 | |
| 94 | return response()->json(['status' => $result ? 'ok' : 'ko']); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * @param Request $request |
| 99 | * @param null $id |
| 100 | * @return array |
| 101 | */ |
| 102 | protected function validateData(Request $request, $id = null) |
| 103 | { |
| 104 | $rules = [ |
| 105 | 'name' => 'required|string', |
| 106 | 'service' => 'required|string', |
| 107 | 'type' => 'required|string|in:cron,queue,other', |
| 108 | 'pattern' => 'nullable|string' |
| 109 | ]; |
| 110 | |
| 111 | if ($id != null) { |
| 112 | $rules['id'] = [ |
| 113 | 'required', |
| 114 | Rule::in([$id]) |
| 115 | ]; |
| 116 | } |
| 117 | |
| 118 | $data = $request->validate($rules); |
| 119 | if (empty($data['pattern'])) { |
| 120 | $data['pattern'] = '* * * * *'; |
| 121 | } |
| 122 | |
| 123 | return $data; |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * @param Request $request |
| 128 | * @return JsonResponse |
| 129 | */ |
| 130 | public function stopCommands(string $action, Request $request): jsonResponse |
| 131 | { |
| 132 | $request->merge(['action' => $request->route('action')]); |
| 133 | |
| 134 | $request->validate([ |
| 135 | 'command_ids' => 'required|array', |
| 136 | 'action' => 'required|in:stop,run,pause,play' |
| 137 | ]); |
| 138 | |
| 139 | switch ($action) { |
| 140 | case 'stop' : |
| 141 | DB::table('crons')->whereIn('id', $request->command_ids)->update(['active' => 0]); |
| 142 | break; |
| 143 | |
| 144 | case 'run' : |
| 145 | $commands = Cron::whereIn('id', $request->command_ids)->get(); |
| 146 | $services = $commands->filter(function ($command) { |
| 147 | $command->active = 1; |
| 148 | $command->save(); |
| 149 | |
| 150 | return $command->type == 'queue' && $command->pause == 0; |
| 151 | |
| 152 | })->map(function ($command) { |
| 153 | return [$command->service => 1]; |
| 154 | })->collapse()->toArray(); |
| 155 | |
| 156 | if (count($services) > 0) { |
| 157 | $this->serviceScaleUp($services); |
| 158 | } |
| 159 | break; |
| 160 | |
| 161 | case 'pause' : |
| 162 | DB::table('crons')->whereIn('id', $request->command_ids)->update(['pause' => 1]); |
| 163 | break; |
| 164 | |
| 165 | case 'play' : |
| 166 | $commands = Cron::whereIn('id', $request->command_ids)->get(); |
| 167 | $services = $commands->filter(function ($command) { |
| 168 | $command->pause = 0; |
| 169 | $command->save(); |
| 170 | |
| 171 | return $command->type == 'queue' && $command->active = 1; |
| 172 | |
| 173 | })->map(function ($command) { |
| 174 | return [$command->service => 1]; |
| 175 | })->collapse()->toArray(); |
| 176 | |
| 177 | if (count($services) > 0) { |
| 178 | $this->serviceScaleUp($services); |
| 179 | } |
| 180 | break; |
| 181 | } |
| 182 | |
| 183 | return response()->json(['status' => 'ok']); |
| 184 | |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Get artisan commands with signature |
| 189 | * |
| 190 | * @return Collection |
| 191 | */ |
| 192 | public function getArtisanCommands(): Collection |
| 193 | { |
| 194 | return Cron::all()->map(function ($command) { |
| 195 | $signature = $this->getSignature($command->name); |
| 196 | $history = $this->getMonitoringHistory($command); |
| 197 | |
| 198 | return [ |
| 199 | 'id' => $command->id, |
| 200 | 'name' => $command->name, |
| 201 | 'signature' => $signature, |
| 202 | 'service' => $command->service, |
| 203 | 'type' => $command->type, |
| 204 | 'pattern' => $command->pattern, |
| 205 | 'state' => $command->active, |
| 206 | 'pause' => $command->pause, |
| 207 | 'last_run' => $history->last_run, |
| 208 | 'is_ok' => $history->is_ok, |
| 209 | 'is_running' => $command->running, |
| 210 | 'warnings' => $history->warnings, |
| 211 | 'errors' => $history->errors |
| 212 | ]; |
| 213 | }); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * @param Request $request |
| 218 | * @return JsonResponse |
| 219 | */ |
| 220 | public function execCommand(Request $request): JsonResponse |
| 221 | { |
| 222 | $request->validate([ |
| 223 | 'id' => 'required', |
| 224 | 'command' => 'required|string', |
| 225 | 'service' => 'required|string' |
| 226 | ]); |
| 227 | |
| 228 | try { |
| 229 | |
| 230 | $clientRequest = ClientRequest::createObject('service_docker_scale', "services-exec", [ |
| 231 | 'body' => [ |
| 232 | 'command' => 'php artisan ' . $request->command, |
| 233 | 'service' => $request->service |
| 234 | ] |
| 235 | ]); |
| 236 | |
| 237 | $response = Client::systemSend('post', $clientRequest); |
| 238 | |
| 239 | if (!Str::startsWith($response->code, 2)) { |
| 240 | throw new \Exception('Response code service_docker_scale : ' . $response->code); |
| 241 | } |
| 242 | |
| 243 | return response()->json(['output' => $response->content['output']]); |
| 244 | |
| 245 | } catch (\Exception $e) { |
| 246 | Log::debug('Unable to exec command : ' . var_export(['message' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile()], true)); |
| 247 | return response()->json(['status' => 'ko', 'message' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY); |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * @return \Illuminate\Support\Collection |
| 253 | */ |
| 254 | protected function getSignatures(): Collection |
| 255 | { |
| 256 | |
| 257 | if (Cache::has(self::CACHE_KEY_SIGNATURES)) { |
| 258 | return Cache::get(self::CACHE_KEY_SIGNATURES); |
| 259 | } |
| 260 | |
| 261 | $commands = collect(Artisan::all())->map(function ($class, $key) { |
| 262 | |
| 263 | $reflection = new \ReflectionClass($class); |
| 264 | |
| 265 | try { |
| 266 | $reflectionProperty = $reflection->getProperty('signature'); |
| 267 | $reflectionProperty->setAccessible(true); |
| 268 | |
| 269 | if ($reflectionProperty->getValue($class)) { |
| 270 | return preg_replace('!\s+!', ' ', $reflectionProperty->getValue($class)); |
| 271 | } |
| 272 | |
| 273 | return $class->getName(); |
| 274 | |
| 275 | } catch (ReflectionException $e) { |
| 276 | //Log::debug("Artisan Command $key : {$e->getMessage()}"); |
| 277 | } |
| 278 | |
| 279 | })->reject(function ($signature) { |
| 280 | return is_null($signature); |
| 281 | })->sort(); |
| 282 | |
| 283 | Cache::put(self::CACHE_KEY_SIGNATURES, $commands, now()->addDay(1)); |
| 284 | |
| 285 | return $commands; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * @param string $command |
| 290 | * @return string|null |
| 291 | */ |
| 292 | protected function getSignature(string $command): ?string |
| 293 | { |
| 294 | return $this->getSignatures()->get($command); |
| 295 | } |
| 296 | |
| 297 | /** |
| 298 | * @param $command |
| 299 | * @return \stdClass |
| 300 | */ |
| 301 | protected function getMonitoringHistory($command): \stdClass |
| 302 | { |
| 303 | $is_ok = false; |
| 304 | $errors = 0; |
| 305 | $warnings = 0; |
| 306 | |
| 307 | $monitoring = CommandMonitoring::where('command', $command->name) |
| 308 | ->where('created_at', '>=', Carbon::now()->subDays(self::SUBDAYS_MONITORING)) |
| 309 | ->orderBy('created_at', 'desc') |
| 310 | ->get(); |
| 311 | |
| 312 | $monitoring->each(function ($log) use (&$errors, &$warnings) { |
| 313 | if (!is_null($log->error)) { |
| 314 | $errors++; |
| 315 | } |
| 316 | |
| 317 | if (!is_null($log->warning)) { |
| 318 | $warnings++; |
| 319 | } |
| 320 | }); |
| 321 | |
| 322 | $lastTwo = $monitoring->take(2); |
| 323 | |
| 324 | $lastTwo->each(function ($log) use (&$is_ok) { |
| 325 | if (!$log->running) { |
| 326 | $is_ok = $log->pass; |
| 327 | } |
| 328 | }); |
| 329 | |
| 330 | $last = $lastTwo->first(); |
| 331 | |
| 332 | return (object)[ |
| 333 | 'last_run' => $last ? $last->created_at->toDateTimeString() : null, |
| 334 | 'is_ok' => (int)$is_ok, |
| 335 | 'errors' => $errors, |
| 336 | 'warnings' => $warnings |
| 337 | ]; |
| 338 | } |
| 339 | |
| 340 | protected function serviceScaleUp(array $services) |
| 341 | { |
| 342 | $clientRequest = ClientRequest::createObject('service_docker_scale', "services-scale", [ |
| 343 | 'body' => [ |
| 344 | 'services' => $services |
| 345 | ] |
| 346 | ]); |
| 347 | |
| 348 | $response = Client::systemSend('post', $clientRequest); |
| 349 | |
| 350 | if (!Str::startsWith($response->code, 2)) { |
| 351 | throw new \Exception('unable to scale service '); |
| 352 | } |
| 353 | } |
| 354 | } |