diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 9721d8267..5dced0599 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -976,7 +976,7 @@ private function push_to_docker_registry() } catch (Exception $e) { $this->application_deployment_queue->addLogEntry('Failed to push image to docker registry. Please check debug logs for more information.'); if ($forceFail) { - throw new DeploymentException($e->getMessage(), 69420); + throw new DeploymentException(get_class($e).': '.$e->getMessage(), $e->getCode(), $e); } } } @@ -1655,7 +1655,7 @@ private function rolling_update() } } } catch (Exception $e) { - throw new DeploymentException("Rolling update failed: {$e->getMessage()}", $e->getCode(), $e); + throw new DeploymentException('Rolling update failed ('.get_class($e).'): '.$e->getMessage(), $e->getCode(), $e); } } @@ -1734,8 +1734,7 @@ private function health_check() } } } catch (Exception $e) { - $this->newVersionIsHealthy = false; - throw new DeploymentException("Health check failed: {$e->getMessage()}", $e->getCode(), $e); + throw new DeploymentException('Health check failed ('.get_class($e).'): '.$e->getMessage(), $e->getCode(), $e); } } @@ -3846,7 +3845,7 @@ private function completeDeployment(): void * Fail the deployment. * Sends failure notification and queues next deployment. */ - private function failDeployment(): void + protected function failDeployment(): void { $this->transitionToStatus(ApplicationDeploymentStatus::FAILED); } @@ -3862,28 +3861,28 @@ public function failed(Throwable $exception): void $this->application_deployment_queue->addLogEntry('========================================', 'stderr'); $this->application_deployment_queue->addLogEntry("Deployment failed: {$errorMessage}", 'stderr'); - $this->application_deployment_queue->addLogEntry("Error type: {$errorClass}", 'stderr'); - $this->application_deployment_queue->addLogEntry("Error code: {$errorCode}", 'stderr'); + $this->application_deployment_queue->addLogEntry("Error type: {$errorClass}", 'stderr', hidden: true); + $this->application_deployment_queue->addLogEntry("Error code: {$errorCode}", 'stderr', hidden: true); // Log the exception file and line for debugging - $this->application_deployment_queue->addLogEntry("Location: {$exception->getFile()}:{$exception->getLine()}", 'stderr'); + $this->application_deployment_queue->addLogEntry("Location: {$exception->getFile()}:{$exception->getLine()}", 'stderr', hidden: true); // Log previous exceptions if they exist (for chained exceptions) $previous = $exception->getPrevious(); if ($previous) { - $this->application_deployment_queue->addLogEntry('Caused by:', 'stderr'); + $this->application_deployment_queue->addLogEntry('Caused by:', 'stderr', hidden: true); $previousMessage = $previous->getMessage() ?: 'No message'; $previousClass = get_class($previous); - $this->application_deployment_queue->addLogEntry(" {$previousClass}: {$previousMessage}", 'stderr'); - $this->application_deployment_queue->addLogEntry(" at {$previous->getFile()}:{$previous->getLine()}", 'stderr'); + $this->application_deployment_queue->addLogEntry(" {$previousClass}: {$previousMessage}", 'stderr', hidden: true); + $this->application_deployment_queue->addLogEntry(" at {$previous->getFile()}:{$previous->getLine()}", 'stderr', hidden: true); } // Log first few lines of stack trace for debugging $trace = $exception->getTraceAsString(); $traceLines = explode("\n", $trace); - $this->application_deployment_queue->addLogEntry('Stack trace (first 5 lines):', 'stderr'); + $this->application_deployment_queue->addLogEntry('Stack trace (first 5 lines):', 'stderr', hidden: true); foreach (array_slice($traceLines, 0, 5) as $traceLine) { - $this->application_deployment_queue->addLogEntry(" {$traceLine}", 'stderr'); + $this->application_deployment_queue->addLogEntry(" {$traceLine}", 'stderr', hidden: true); } $this->application_deployment_queue->addLogEntry('========================================', 'stderr'); diff --git a/app/Jobs/ServerManagerJob.php b/app/Jobs/ServerManagerJob.php index 043845c00..45ab1dde8 100644 --- a/app/Jobs/ServerManagerJob.php +++ b/app/Jobs/ServerManagerJob.php @@ -87,7 +87,7 @@ private function dispatchConnectionChecks(Collection $servers): void Log::channel('scheduled-errors')->error('Failed to dispatch ServerConnectionCheck', [ 'server_id' => $server->id, 'server_name' => $server->name, - 'error' => $e->getMessage(), + 'error' => get_class($e).': '.$e->getMessage(), ]); } }); @@ -103,7 +103,7 @@ private function processScheduledTasks(Collection $servers): void Log::channel('scheduled-errors')->error('Error processing server tasks', [ 'server_id' => $server->id, 'server_name' => $server->name, - 'error' => $e->getMessage(), + 'error' => get_class($e).': '.$e->getMessage(), ]); } } diff --git a/tests/Unit/ApplicationDeploymentErrorLoggingTest.php b/tests/Unit/ApplicationDeploymentErrorLoggingTest.php index b2557c4f3..c6210639a 100644 --- a/tests/Unit/ApplicationDeploymentErrorLoggingTest.php +++ b/tests/Unit/ApplicationDeploymentErrorLoggingTest.php @@ -4,7 +4,6 @@ use App\Jobs\ApplicationDeploymentJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; -use Mockery; /** * Test to verify that deployment errors are properly logged with comprehensive details. @@ -33,8 +32,8 @@ // Capture all log entries $mockQueue->shouldReceive('addLogEntry') - ->withArgs(function ($message, $type = 'stdout') use (&$logEntries) { - $logEntries[] = ['message' => $message, 'type' => $type]; + ->withArgs(function ($message, $type = 'stdout', $hidden = false) use (&$logEntries) { + $logEntries[] = ['message' => $message, 'type' => $type, 'hidden' => $hidden]; return true; }) @@ -47,6 +46,9 @@ $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('dockerfile'); + $mockApplication->shouldReceive('setAttribute') + ->with('build_pack', 'dockerfile') + ->andReturnSelf(); $mockApplication->build_pack = 'dockerfile'; $mockSettings = Mockery::mock(); @@ -56,6 +58,8 @@ $mockSettings->shouldReceive('getAttribute') ->with('custom_internal_name') ->andReturn(''); + $mockSettings->shouldReceive('setAttribute') + ->andReturnSelf(); $mockSettings->is_consistent_container_name_enabled = false; $mockSettings->custom_internal_name = ''; @@ -67,7 +71,7 @@ $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); - $reflection = new \ReflectionClass($job); + $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); $queueProperty = $reflection->getProperty('application_deployment_queue'); $queueProperty->setAccessible(true); @@ -81,6 +85,10 @@ $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 0); + $containerNameProperty = $reflection->getProperty('container_name'); + $containerNameProperty->setAccessible(true); + $containerNameProperty->setValue($job, 'test-container'); + // Mock the failDeployment method to prevent errors $job->shouldReceive('failDeployment')->andReturn(); $job->shouldReceive('execute_remote_command')->andReturn(); @@ -106,6 +114,26 @@ // Verify stderr type is used for error logging $stderrEntries = array_filter($logEntries, fn ($entry) => $entry['type'] === 'stderr'); expect(count($stderrEntries))->toBeGreaterThan(0); + + // Verify that the main error message is NOT hidden + $mainErrorEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Deployment failed: Failed to start container')); + expect($mainErrorEntry['hidden'])->toBeFalse(); + + // Verify that technical details ARE hidden + $errorTypeEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Error type:')); + expect($errorTypeEntry['hidden'])->toBeTrue(); + + $errorCodeEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Error code:')); + expect($errorCodeEntry['hidden'])->toBeTrue(); + + $locationEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Location:')); + expect($locationEntry['hidden'])->toBeTrue(); + + $stackTraceEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Stack trace')); + expect($stackTraceEntry['hidden'])->toBeTrue(); + + $causedByEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Caused by:')); + expect($causedByEntry['hidden'])->toBeTrue(); }); it('handles exceptions with no message gracefully', function () { @@ -115,8 +143,8 @@ $logEntries = []; $mockQueue->shouldReceive('addLogEntry') - ->withArgs(function ($message, $type = 'stdout') use (&$logEntries) { - $logEntries[] = ['message' => $message, 'type' => $type]; + ->withArgs(function ($message, $type = 'stdout', $hidden = false) use (&$logEntries) { + $logEntries[] = ['message' => $message, 'type' => $type, 'hidden' => $hidden]; return true; }) @@ -128,6 +156,9 @@ $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('dockerfile'); + $mockApplication->shouldReceive('setAttribute') + ->with('build_pack', 'dockerfile') + ->andReturnSelf(); $mockApplication->build_pack = 'dockerfile'; $mockSettings = Mockery::mock(); @@ -137,6 +168,8 @@ $mockSettings->shouldReceive('getAttribute') ->with('custom_internal_name') ->andReturn(''); + $mockSettings->shouldReceive('setAttribute') + ->andReturnSelf(); $mockSettings->is_consistent_container_name_enabled = false; $mockSettings->custom_internal_name = ''; @@ -147,7 +180,7 @@ $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); - $reflection = new \ReflectionClass($job); + $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); $queueProperty = $reflection->getProperty('application_deployment_queue'); $queueProperty->setAccessible(true); @@ -161,6 +194,10 @@ $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 0); + $containerNameProperty = $reflection->getProperty('container_name'); + $containerNameProperty->setAccessible(true); + $containerNameProperty->setValue($job, 'test-container'); + $job->shouldReceive('failDeployment')->andReturn(); $job->shouldReceive('execute_remote_command')->andReturn(); @@ -197,8 +234,8 @@ $logEntries = []; $mockQueue->shouldReceive('addLogEntry') - ->withArgs(function ($message, $type = 'stdout') use (&$logEntries) { - $logEntries[] = ['message' => $message, 'type' => $type]; + ->withArgs(function ($message, $type = 'stdout', $hidden = false) use (&$logEntries) { + $logEntries[] = ['message' => $message, 'type' => $type, 'hidden' => $hidden]; return true; }) @@ -210,6 +247,9 @@ $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('dockerfile'); + $mockApplication->shouldReceive('setAttribute') + ->with('build_pack', 'dockerfile') + ->andReturnSelf(); $mockApplication->build_pack = 'dockerfile'; $mockSettings = Mockery::mock(); @@ -219,6 +259,8 @@ $mockSettings->shouldReceive('getAttribute') ->with('custom_internal_name') ->andReturn(''); + $mockSettings->shouldReceive('setAttribute') + ->andReturnSelf(); $mockSettings->is_consistent_container_name_enabled = false; $mockSettings->custom_internal_name = ''; @@ -229,7 +271,7 @@ $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); - $reflection = new \ReflectionClass($job); + $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); $queueProperty = $reflection->getProperty('application_deployment_queue'); $queueProperty->setAccessible(true); @@ -243,6 +285,10 @@ $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 0); + $containerNameProperty = $reflection->getProperty('container_name'); + $containerNameProperty->setAccessible(true); + $containerNameProperty->setValue($job, 'test-container'); + $job->shouldReceive('failDeployment')->andReturn(); $job->shouldReceive('execute_remote_command')->andReturn(); @@ -256,3 +302,43 @@ // Should log error code 0 (not skip it) expect($errorMessageString)->toContain('Error code: 0'); }); + +it('preserves original exception type in wrapped DeploymentException messages', function () { + // Verify that when wrapping exceptions, the original exception type is included in the message + $originalException = new \RuntimeException('Connection timeout'); + + // Test rolling update scenario + $wrappedException = new DeploymentException( + 'Rolling update failed ('.get_class($originalException).'): '.$originalException->getMessage(), + $originalException->getCode(), + $originalException + ); + + expect($wrappedException->getMessage())->toContain('RuntimeException'); + expect($wrappedException->getMessage())->toContain('Connection timeout'); + expect($wrappedException->getPrevious())->toBe($originalException); + + // Test health check scenario + $healthCheckException = new \InvalidArgumentException('Invalid health check URL'); + $wrappedHealthCheck = new DeploymentException( + 'Health check failed ('.get_class($healthCheckException).'): '.$healthCheckException->getMessage(), + $healthCheckException->getCode(), + $healthCheckException + ); + + expect($wrappedHealthCheck->getMessage())->toContain('InvalidArgumentException'); + expect($wrappedHealthCheck->getMessage())->toContain('Invalid health check URL'); + expect($wrappedHealthCheck->getPrevious())->toBe($healthCheckException); + + // Test docker registry push scenario + $registryException = new \RuntimeException('Failed to authenticate'); + $wrappedRegistry = new DeploymentException( + get_class($registryException).': '.$registryException->getMessage(), + $registryException->getCode(), + $registryException + ); + + expect($wrappedRegistry->getMessage())->toContain('RuntimeException'); + expect($wrappedRegistry->getMessage())->toContain('Failed to authenticate'); + expect($wrappedRegistry->getPrevious())->toBe($registryException); +});