diff --git a/enterprise/integrations/azure_devops/azure_devops_manager.py b/enterprise/integrations/azure_devops/azure_devops_manager.py index bd5d32a4c0..07e569cc5e 100644 --- a/enterprise/integrations/azure_devops/azure_devops_manager.py +++ b/enterprise/integrations/azure_devops/azure_devops_manager.py @@ -230,8 +230,8 @@ class AzureDevOpsManager(Manager[AzureDevOpsViewType]): await self.send_message(msg_info, azure_view) - except Exception as e: - logger.exception(f'[Azure DevOps] Error starting job: {e}') + except Exception: + logger.exception('[Azure DevOps] Error starting job', stack_info=True) await self.send_message( 'Uh oh! There was an unexpected error starting the job :(', azure_view, diff --git a/enterprise/integrations/azure_devops/azure_devops_v1_callback_processor.py b/enterprise/integrations/azure_devops/azure_devops_v1_callback_processor.py index a872ce6e35..c0efd71980 100644 --- a/enterprise/integrations/azure_devops/azure_devops_v1_callback_processor.py +++ b/enterprise/integrations/azure_devops/azure_devops_v1_callback_processor.py @@ -142,14 +142,15 @@ class AzureDevOpsV1CallbackProcessor(EventCallbackProcessor): error_detail += f': {e.response.text}' except Exception: pass - _logger.error( - '[Azure DevOps V1] HTTP error: %s', error_detail, exc_info=True + _logger.exception( + '[Azure DevOps V1] HTTP error', + stack_info=True, ) - raise Exception(f'Failed to send message to agent server: {error_detail}') + raise Exception('Failed to send message to agent server') from e except httpx.TimeoutException: raise Exception(f'Request timeout after 30 seconds to {url}') except httpx.RequestError as e: - raise Exception(f'Request error to {url}: {e}') + raise Exception(f'Request error to {url}') from e async def _request_summary(self, conversation_id: UUID) -> str: from openhands.app_server.config import ( diff --git a/enterprise/integrations/bitbucket/bitbucket_manager.py b/enterprise/integrations/bitbucket/bitbucket_manager.py index 479c7ed98b..4b7ea25beb 100644 --- a/enterprise/integrations/bitbucket/bitbucket_manager.py +++ b/enterprise/integrations/bitbucket/bitbucket_manager.py @@ -256,8 +256,8 @@ class BitbucketManager(Manager[BitbucketViewType]): await self.send_message(msg_info, bitbucket_view) - except Exception as e: - logger.exception(f'[Bitbucket] Error starting job: {e}') + except Exception: + logger.exception('[Bitbucket] Error starting job', stack_info=True) await self.send_message( 'Uh oh! There was an unexpected error starting the job :(', bitbucket_view, diff --git a/enterprise/integrations/bitbucket/bitbucket_v1_callback_processor.py b/enterprise/integrations/bitbucket/bitbucket_v1_callback_processor.py index f86a199138..cac823037d 100644 --- a/enterprise/integrations/bitbucket/bitbucket_v1_callback_processor.py +++ b/enterprise/integrations/bitbucket/bitbucket_v1_callback_processor.py @@ -132,12 +132,17 @@ class BitbucketV1CallbackProcessor(EventCallbackProcessor): error_detail += f': {e.response.text}' except Exception: # noqa: BLE001 pass - _logger.error('[Bitbucket V1] HTTP error: %s', error_detail, exc_info=True) - raise Exception(f'Failed to send message to agent server: {error_detail}') + _logger.exception( + '[Bitbucket V1] HTTP error', + stack_info=True, + ) + raise Exception( + f'Failed to send message to agent server: {error_detail}' + ) from e except httpx.TimeoutException: raise Exception(f'Request timeout after 30 seconds to {url}') except httpx.RequestError as e: - raise Exception(f'Request error to {url}: {e}') + raise Exception(f'Request error to {url}') from e async def _request_summary(self, conversation_id: UUID) -> str: from openhands.app_server.config import ( diff --git a/enterprise/integrations/bitbucket_data_center/bitbucket_dc_manager.py b/enterprise/integrations/bitbucket_data_center/bitbucket_dc_manager.py index b904cb8fde..305759225f 100644 --- a/enterprise/integrations/bitbucket_data_center/bitbucket_dc_manager.py +++ b/enterprise/integrations/bitbucket_data_center/bitbucket_dc_manager.py @@ -443,8 +443,8 @@ class BitbucketDCManager(Manager[BitbucketDCViewType]): await self.send_message(msg_info, bitbucket_view) - except Exception as e: - logger.exception(f'[Bitbucket DC] Error starting job: {e}') + except Exception: + logger.exception('[Bitbucket DC] Error starting job', stack_info=True) await self.send_message( 'Uh oh! There was an unexpected error starting the job :(', bitbucket_view, diff --git a/enterprise/integrations/bitbucket_data_center/bitbucket_dc_v1_callback_processor.py b/enterprise/integrations/bitbucket_data_center/bitbucket_dc_v1_callback_processor.py index 0750a127e3..9cf7080c5c 100644 --- a/enterprise/integrations/bitbucket_data_center/bitbucket_dc_v1_callback_processor.py +++ b/enterprise/integrations/bitbucket_data_center/bitbucket_dc_v1_callback_processor.py @@ -132,14 +132,17 @@ class BitbucketDCV1CallbackProcessor(EventCallbackProcessor): error_detail += f': {e.response.text}' except Exception: # noqa: BLE001 pass - _logger.error( - '[Bitbucket DC V1] HTTP error: %s', error_detail, exc_info=True + _logger.exception( + '[Bitbucket DC V1] HTTP error', + stack_info=True, ) - raise Exception(f'Failed to send message to agent server: {error_detail}') + raise Exception( + f'Failed to send message to agent server: {error_detail}' + ) from e except httpx.TimeoutException: raise Exception(f'Request timeout after 30 seconds to {url}') except httpx.RequestError as e: - raise Exception(f'Request error to {url}: {e}') + raise Exception(f'Request error to {url}') from e async def _request_summary(self, conversation_id: UUID) -> str: from openhands.app_server.config import ( diff --git a/enterprise/integrations/github/data_collector.py b/enterprise/integrations/github/data_collector.py index e26459e4da..8407e4274e 100644 --- a/enterprise/integrations/github/data_collector.py +++ b/enterprise/integrations/github/data_collector.py @@ -552,7 +552,7 @@ class GitHubDataCollector: break except Exception: - logger.warning('Error fetching PR data', exc_info=True) + logger.warning('Error fetching PR data') return if not pr_data or not repo_data: diff --git a/enterprise/integrations/github/github_manager.py b/enterprise/integrations/github/github_manager.py index 3308e93ab2..3f069b5d7a 100644 --- a/enterprise/integrations/github/github_manager.py +++ b/enterprise/integrations/github/github_manager.py @@ -199,10 +199,11 @@ class GithubManager(Manager[GithubViewType]): repo = github_client.get_repo(full_repo_name) issue = repo.get_issue(number=issue_number) issue.create_comment(get_user_not_found_message(username)) - except Exception as e: - logger.error( + except Exception: + logger.exception( f'[GitHub] Failed to send user not found message to {username} ' - f'on {full_repo_name}#{issue_number}: {e}' + f'on {full_repo_name}#{issue_number}', + stack_info=True, ) async def is_job_requested(self, message: Message) -> bool: @@ -262,8 +263,9 @@ class GithubManager(Manager[GithubViewType]): try: await self.data_collector.process_payload(message) except Exception: - logger.error( - '[Github]: Error processing payload for gh interaction', exc_info=True + logger.exception( + '[Github]: Error processing payload for gh interaction', + stack_info=True, ) if await self.is_job_requested(message): @@ -429,7 +431,7 @@ class GithubManager(Manager[GithubViewType]): await self.send_message(msg_info, github_view) except Exception: - logger.exception('[Github]: Error starting job') + logger.exception('[Github]: Error starting job', stack_info=True) await self.send_message( 'Uh oh! There was an unexpected error starting the job :(', github_view ) diff --git a/enterprise/integrations/github/github_service.py b/enterprise/integrations/github/github_service.py index 2529e268c3..61314b824e 100644 --- a/enterprise/integrations/github/github_service.py +++ b/enterprise/integrations/github/github_service.py @@ -153,9 +153,9 @@ class SaaSGitHubService(GitHubService): f'Determined external_auth_id from Keycloak token: {self.external_auth_id}' ) return self.external_auth_id - except Exception as e: + except Exception: logger.warning( - f'Could not determine external_auth_id from token: {e}', + 'Could not determine external_auth_id from token', exc_info=True, ) return None diff --git a/enterprise/integrations/github/github_v1_callback_processor.py b/enterprise/integrations/github/github_v1_callback_processor.py index 0f7dd1858b..c8915434f6 100644 --- a/enterprise/integrations/github/github_v1_callback_processor.py +++ b/enterprise/integrations/github/github_v1_callback_processor.py @@ -207,27 +207,27 @@ class GithubV1CallbackProcessor(EventCallbackProcessor): except Exception: # noqa: BLE001 pass - _logger.error( + _logger.exception( '[GitHub V1] HTTP error fetching final response from %s: %s. ' 'Response headers: %s', url, error_detail, dict(e.response.headers), - exc_info=True, + stack_info=True, ) raise Exception( f'Failed to fetch final response from agent server: {error_detail}' - ) + ) from e except httpx.TimeoutException: error_detail = f'Request timeout after 30 seconds to {url}' - _logger.error('[GitHub V1] %s', error_detail, exc_info=True) + _logger.exception('[GitHub V1] %s', error_detail, stack_info=True) raise Exception(error_detail) except httpx.RequestError as e: - error_detail = f'Request error to {url}: {str(e)}' - _logger.error('[GitHub V1] %s', error_detail, exc_info=True) - raise Exception(error_detail) + error_detail = f'Request error to {url}' + _logger.exception('[GitHub V1] %s', error_detail, stack_info=True) + raise Exception(error_detail) from e # ------------------------------------------------------------------------- # Final response orchestration diff --git a/enterprise/integrations/gitlab/gitlab_manager.py b/enterprise/integrations/gitlab/gitlab_manager.py index 0e515c8d34..fcc7f551bf 100644 --- a/enterprise/integrations/gitlab/gitlab_manager.py +++ b/enterprise/integrations/gitlab/gitlab_manager.py @@ -257,8 +257,8 @@ class GitlabManager(Manager[GitlabViewType]): # Send the acknowledgment message await self.send_message(msg_info, gitlab_view) - except Exception as e: - logger.exception(f'[GitLab] Error starting job: {str(e)}') + except Exception: + logger.exception('[GitLab] Error starting job', stack_info=True) await self.send_message( 'Uh oh! There was an unexpected error starting the job :(', gitlab_view ) diff --git a/enterprise/integrations/gitlab/gitlab_service.py b/enterprise/integrations/gitlab/gitlab_service.py index ef828160b0..8a88b4d1c4 100644 --- a/enterprise/integrations/gitlab/gitlab_service.py +++ b/enterprise/integrations/gitlab/gitlab_service.py @@ -567,8 +567,8 @@ class SaaSGitLabService(GitLabService): params = {'body': body} await self._make_request(url=url, params=params, method=RequestMethod.POST) - except Exception as e: - logger.exception(f'[GitLab]: Reply to issue failed {e}') + except Exception: + logger.exception('[GitLab]: Reply to issue failed', stack_info=True) async def reply_to_mr( self, project_id: str, merge_request_iid: str, discussion_id: str, body: str @@ -581,8 +581,8 @@ class SaaSGitLabService(GitLabService): params = {'body': body} await self._make_request(url=url, params=params, method=RequestMethod.POST) - except Exception as e: - logger.exception(f'[GitLab]: Reply to MR failed {e}') + except Exception: + logger.exception('[GitLab]: Reply to MR failed', stack_info=True) async def get_user_resources_with_admin_access( self, diff --git a/enterprise/integrations/gitlab/gitlab_v1_callback_processor.py b/enterprise/integrations/gitlab/gitlab_v1_callback_processor.py index 21f282dd69..7d4fe6eff2 100644 --- a/enterprise/integrations/gitlab/gitlab_v1_callback_processor.py +++ b/enterprise/integrations/gitlab/gitlab_v1_callback_processor.py @@ -180,36 +180,38 @@ class GitlabV1CallbackProcessor(EventCallbackProcessor): except Exception: # noqa: BLE001 pass - _logger.error( + _logger.exception( '[GitLab V1] HTTP error sending message to %s: %s. ' 'Request payload: %s. Response headers: %s', url, error_detail, payload, dict(e.response.headers), - exc_info=True, + stack_info=True, ) - raise Exception(f'Failed to send message to agent server: {error_detail}') + raise Exception( + f'Failed to send message to agent server: {error_detail}' + ) from e except httpx.TimeoutException: error_detail = f'Request timeout after 30 seconds to {url}' - _logger.error( + _logger.exception( '[GitLab V1] %s. Request payload: %s', error_detail, payload, - exc_info=True, + stack_info=True, ) raise Exception(error_detail) except httpx.RequestError as e: error_detail = f'Request error to {url}: {str(e)}' - _logger.error( + _logger.exception( '[GitLab V1] %s. Request payload: %s', error_detail, payload, - exc_info=True, + stack_info=True, ) - raise Exception(error_detail) + raise Exception(error_detail) from e # ------------------------------------------------------------------------- # Summary orchestration diff --git a/enterprise/integrations/jira/jira_manager.py b/enterprise/integrations/jira/jira_manager.py index 2c8a9aea38..1eee0e1bee 100644 --- a/enterprise/integrations/jira/jira_manager.py +++ b/enterprise/integrations/jira/jira_manager.py @@ -155,11 +155,13 @@ class JiraManager(Manager[JiraViewInterface]): ) await self._send_error_from_payload(payload, workspace, str(e)) return - except Exception as e: - logger.error( + except Exception: + logger.exception( '[Jira] Unexpected error creating view', - extra={'issue_key': payload.issue_key, 'error': str(e)}, - exc_info=True, + extra={ + 'issue_key': payload.issue_key, + }, + stack_info=True, ) await self._send_error_from_payload( payload, @@ -312,11 +314,13 @@ class JiraManager(Manager[JiraViewInterface]): ) msg_info = str(e) - except Exception as e: - logger.error( + except Exception: + logger.exception( '[Jira] Unexpected error starting job', - extra={'issue_key': view.payload.issue_key, 'error': str(e)}, - exc_info=True, + extra={ + 'issue_key': view.payload.issue_key, + }, + stack_info=True, ) msg_info = 'Sorry, there was an unexpected error starting the job. Please try again.' @@ -366,10 +370,13 @@ class JiraManager(Manager[JiraViewInterface]): svc_acc_email=view.jira_workspace.svc_acc_email, svc_acc_api_key=api_key, ) - except Exception as e: - logger.error( + except Exception: + logger.exception( '[Jira] Failed to send comment', - extra={'issue_key': view.payload.issue_key, 'error': str(e)}, + extra={ + 'issue_key': view.payload.issue_key, + }, + stack_info=True, ) async def _send_error_from_payload( @@ -388,10 +395,13 @@ class JiraManager(Manager[JiraViewInterface]): svc_acc_email=workspace.svc_acc_email, svc_acc_api_key=api_key, ) - except Exception as e: - logger.error( + except Exception: + logger.exception( '[Jira] Failed to send error comment', - extra={'issue_key': payload.issue_key, 'error': str(e)}, + extra={ + 'issue_key': payload.issue_key, + }, + stack_info=True, ) def get_workspace_name_from_payload(self, payload: dict) -> str | None: diff --git a/enterprise/integrations/jira/jira_v1_callback_processor.py b/enterprise/integrations/jira/jira_v1_callback_processor.py index fd6658081c..d23c55930c 100644 --- a/enterprise/integrations/jira/jira_v1_callback_processor.py +++ b/enterprise/integrations/jira/jira_v1_callback_processor.py @@ -86,7 +86,7 @@ class JiraV1CallbackProcessor(EventCallbackProcessor): detail=summary, ) except Exception as e: - _logger.exception(f'[Jira] Failed to post summary: {e}', stack_info=True) + _logger.exception('[Jira] Failed to post summary', stack_info=True) return EventCallbackResult( status=EventCallbackResultStatus.ERROR, event_callback_id=callback.id, @@ -199,7 +199,9 @@ class JiraV1CallbackProcessor(EventCallbackProcessor): dict(e.response.headers), stack_info=True, ) - raise Exception(f'Failed to send message to agent server: {error_detail}') + raise Exception( + f'Failed to send message to agent server: {error_detail}' + ) from e except httpx.TimeoutException: error_detail = ( diff --git a/enterprise/integrations/jira/jira_view.py b/enterprise/integrations/jira/jira_view.py index 88086b1b3c..1820745891 100644 --- a/enterprise/integrations/jira/jira_view.py +++ b/enterprise/integrations/jira/jira_view.py @@ -130,24 +130,26 @@ class JiraNewConversationView(JiraViewInterface): return self._issue_title, self._issue_description except httpx.HTTPStatusError as e: - logger.error( + logger.exception( '[Jira] Failed to fetch issue details', extra={ 'issue_key': self.payload.issue_key, 'status': e.response.status_code, }, + stack_info=True, ) - raise StartingConvoException( - f'Failed to fetch issue details: HTTP {e.response.status_code}' - ) + raise StartingConvoException('Failed to fetch issue details: HTTP') from e except Exception as e: if isinstance(e, StartingConvoException): raise - logger.error( + logger.exception( '[Jira] Failed to fetch issue details', - extra={'issue_key': self.payload.issue_key, 'error': str(e)}, + extra={ + 'issue_key': self.payload.issue_key, + }, + stack_info=True, ) - raise StartingConvoException(f'Failed to fetch issue details: {str(e)}') + raise StartingConvoException('Failed to fetch issue details') from e async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]: """Get instructions for the conversation. @@ -512,7 +514,7 @@ class JiraFactory: except StartingConvoException: raise # Re-raise with original message except Exception as e: - raise StartingConvoException(f'Failed to fetch issue details: {str(e)}') + raise StartingConvoException('Failed to fetch issue details') from e # Infer and select repository selected_repo = await JiraFactory._infer_repository( diff --git a/enterprise/integrations/jira_dc/jira_dc_manager.py b/enterprise/integrations/jira_dc/jira_dc_manager.py index 10e55c8f2d..21161a2b59 100644 --- a/enterprise/integrations/jira_dc/jira_dc_manager.py +++ b/enterprise/integrations/jira_dc/jira_dc_manager.py @@ -436,9 +436,10 @@ class JiraDcManager(Manager[JiraDcViewInterface]): service_account = resolve_jira_dc_service_account( workspace, self.token_manager ) - except Exception as e: - logger.error( - f'[Jira DC] Service account configuration is invalid: {str(e)}' + except Exception: + logger.exception( + '[Jira DC] Service account configuration is invalid', + stack_info=True, ) return @@ -487,8 +488,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]): service_account.api_key, bot_email=service_account.email, ) - except Exception as e: - logger.error(f'[Jira DC] Failed to get issue context: {str(e)}') + except Exception: + logger.exception('[Jira DC] Failed to get issue context', stack_info=True) await self._send_error_comment( job_context, 'Failed to retrieve issue details. Please check the issue key and try again.', @@ -504,9 +505,10 @@ class JiraDcManager(Manager[JiraDcViewInterface]): jira_dc_user, workspace, ) - except Exception as e: - logger.error( - f'[Jira DC] Failed to create jira dc view: {str(e)}', exc_info=True + except Exception: + logger.exception( + '[Jira DC] Failed to create jira dc view', + stack_info=True, ) await self._send_error_comment( job_context, @@ -566,8 +568,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]): ) return False - except Exception as e: - logger.error(f'[Jira DC] Error in is_job_requested: {str(e)}') + except Exception: + logger.exception('[Jira DC] Error in is_job_requested', stack_info=True) return False async def start_job(self, jira_dc_view: JiraDcViewInterface) -> None: @@ -608,9 +610,10 @@ class JiraDcManager(Manager[JiraDcViewInterface]): logger.warning(f'[Jira DC] User token unavailable: {str(e)}') msg_info = get_jira_dc_relink_message(jira_dc_view.job_context.display_name) - except Exception as e: - logger.error( - f'[Jira DC] Unexpected error starting job: {str(e)}', exc_info=True + except Exception: + logger.exception( + '[Jira DC] Unexpected error starting job', + stack_info=True, ) msg_info = 'Sorry, there was an unexpected error starting the job. Please try again.' @@ -625,8 +628,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]): base_api_url=jira_dc_view.job_context.base_api_url, svc_acc_api_key=service_account.api_key, ) - except Exception as e: - logger.error(f'[Jira] Failed to send response message: {str(e)}') + except Exception: + logger.exception('[Jira] Failed to send response message', stack_info=True) async def _resolve_service_account_mentions(self, payload: Dict) -> set[str] | None: """Best-effort bot identifiers (username + Jira key) for a picker mention. @@ -1007,8 +1010,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]): base_api_url=job_context.base_api_url, svc_acc_api_key=service_account.api_key, ) - except Exception as e: - logger.error(f'[Jira DC] Failed to send error comment: {str(e)}') + except Exception: + logger.exception('[Jira DC] Failed to send error comment', stack_info=True) async def _send_repo_selection_comment( self, @@ -1055,7 +1058,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]): f'[Jira] Sent repository selection comment for issue {jira_dc_view.job_context.issue_key}' ) - except Exception as e: - logger.error( - f'[Jira] Failed to send repository selection comment: {str(e)}' + except Exception: + logger.exception( + '[Jira] Failed to send repository selection comment', + stack_info=True, ) diff --git a/enterprise/integrations/jira_dc/jira_dc_v1_callback_processor.py b/enterprise/integrations/jira_dc/jira_dc_v1_callback_processor.py index 83203efe52..93644ecb55 100644 --- a/enterprise/integrations/jira_dc/jira_dc_v1_callback_processor.py +++ b/enterprise/integrations/jira_dc/jira_dc_v1_callback_processor.py @@ -96,7 +96,7 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor): detail=summary, ) except Exception as e: - _logger.exception(f'[Jira DC] Failed to post summary: {e}', stack_info=True) + _logger.exception('[Jira DC] Failed to post summary', stack_info=True) return EventCallbackResult( status=EventCallbackResultStatus.ERROR, event_callback_id=callback.id, @@ -209,7 +209,9 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor): dict(e.response.headers), stack_info=True, ) - raise Exception(f'Failed to send message to agent server: {error_detail}') + raise Exception( + f'Failed to send message to agent server: {error_detail}' + ) from e except httpx.TimeoutException: error_detail = ( diff --git a/enterprise/integrations/jira_dc/jira_dc_view.py b/enterprise/integrations/jira_dc/jira_dc_view.py index 7fd8c1a109..519dc0746b 100644 --- a/enterprise/integrations/jira_dc/jira_dc_view.py +++ b/enterprise/integrations/jira_dc/jira_dc_view.py @@ -334,12 +334,13 @@ class JiraDcExistingConversationView(JiraDcViewInterface): f'[Jira DC] Sent message to existing conversation {self.conversation_id}' ) except httpx.HTTPStatusError as e: - logger.error( - f'[Jira DC] Failed to send message: HTTP {e.response.status_code}' + logger.exception( + f'[Jira DC] Failed to send message: HTTP {e.response.status_code}', + stack_info=True, ) raise - except Exception as e: - logger.error(f'[Jira DC] Failed to send message: {e}') + except Exception: + logger.exception('[Jira DC] Failed to send message', stack_info=True) raise def get_response_msg(self) -> str: diff --git a/enterprise/integrations/resolver_org_router.py b/enterprise/integrations/resolver_org_router.py index c65eb05e74..5b81763360 100644 --- a/enterprise/integrations/resolver_org_router.py +++ b/enterprise/integrations/resolver_org_router.py @@ -70,9 +70,9 @@ async def resolve_org_for_repo( f'for {provider}/{git_org} (user {keycloak_user_id})', ) return claim.org_id - except Exception as e: - logger.error( - f'[OrgResolver] Error resolving org for {provider}/{git_org}: {e}', - exc_info=True, + except Exception: + logger.exception( + f'[OrgResolver] Error resolving org for {provider}/{git_org}', + stack_info=True, ) return None diff --git a/enterprise/integrations/slack/slack_manager.py b/enterprise/integrations/slack/slack_manager.py index 97bfc6d064..394f77751e 100644 --- a/enterprise/integrations/slack/slack_manager.py +++ b/enterprise/integrations/slack/slack_manager.py @@ -137,19 +137,19 @@ class SlackManager(Manager[SlackViewInterface]): }, ) except Exception as e: - logger.error( + logger.exception( 'slack_store_user_msg_failed', extra={ 'message_ts': message_ts, 'thread_ts': thread_ts, 'key': key, - 'error': str(e), }, + stack_info=True, ) raise SlackError( SlackErrorCode.REDIS_STORE_FAILED, log_context={'message_ts': message_ts, 'thread_ts': thread_ts}, - ) + ) from e async def _retrieve_user_msg_for_form( self, message_ts: str, thread_ts: str | None @@ -200,19 +200,19 @@ class SlackManager(Manager[SlackViewInterface]): except SlackError: raise except Exception as e: - logger.error( + logger.exception( 'slack_retrieve_user_msg_failed', extra={ 'message_ts': message_ts, 'thread_ts': thread_ts, 'key': key, - 'error': str(e), }, + stack_info=True, ) raise SlackError( SlackErrorCode.REDIS_RETRIEVE_FAILED, log_context={'message_ts': message_ts, 'thread_ts': thread_ts}, - ) + ) from e async def _claim_form_interaction( self, team_id: str, channel_id: str, message_ts: str, thread_ts: str | None @@ -256,19 +256,19 @@ class SlackManager(Manager[SlackViewInterface]): ) return False except Exception as e: - logger.error( + logger.exception( 'slack_claim_form_interaction_failed', extra={ 'message_ts': message_ts, 'thread_ts': thread_ts, 'key': key, - 'error': str(e), }, + stack_info=True, ) raise SlackError( SlackErrorCode.REDIS_STORE_FAILED, log_context={'message_ts': message_ts, 'thread_ts': thread_ts}, - ) + ) from e async def _replace_repo_selection_form( self, response_url: str | None, selected_repository: str | None @@ -464,10 +464,9 @@ class SlackManager(Manager[SlackViewInterface]): except SlackError as e: await self.handle_slack_error(message.message, e) - except Exception as e: + except Exception: logger.exception( - 'slack_unexpected_error', - extra={'error': str(e), **message.message}, + 'slack_unexpected_error', extra={**message.message}, stack_info=True ) await self.handle_slack_error( message.message, @@ -566,10 +565,9 @@ class SlackManager(Manager[SlackViewInterface]): except SlackError as e: await self.handle_slack_error(payload, e) return - except Exception as e: + except Exception: logger.exception( - 'slack_unexpected_error', - extra={'error': str(e), **payload}, + 'slack_unexpected_error', extra={**payload}, stack_info=True ) await self.handle_slack_error( payload, SlackError(SlackErrorCode.UNEXPECTED_ERROR) @@ -861,7 +859,7 @@ class SlackManager(Manager[SlackViewInterface]): await self.send_message(msg_info, slack_view) except Exception: - logger.exception('[Slack]: Error starting job') + logger.exception('[Slack]: Error starting job', stack_info=True) await self.send_message( 'Uh oh! There was an unexpected error starting the job :(', slack_view ) diff --git a/enterprise/integrations/slack/slack_v1_callback_processor.py b/enterprise/integrations/slack/slack_v1_callback_processor.py index 65b2998ddf..7314e74cae 100644 --- a/enterprise/integrations/slack/slack_v1_callback_processor.py +++ b/enterprise/integrations/slack/slack_v1_callback_processor.py @@ -138,8 +138,10 @@ class SlackV1CallbackProcessor(EventCallbackProcessor): channel_id, ) - except Exception as e: - _logger.error('[Slack V1] Failed to post message to Slack: %s', e) + except Exception: + _logger.exception( + '[Slack V1] Failed to post message to Slack', stack_info=True + ) raise # ------------------------------------------------------------------------- @@ -186,27 +188,27 @@ class SlackV1CallbackProcessor(EventCallbackProcessor): except Exception: # noqa: BLE001 pass - _logger.error( + _logger.exception( '[Slack V1] HTTP error fetching final response from %s: %s. ' 'Response headers: %s', url, error_detail, dict(e.response.headers), - exc_info=True, + stack_info=True, ) raise Exception( f'Failed to fetch final response from agent server: {error_detail}' - ) + ) from e except httpx.TimeoutException: error_detail = f'Request timeout after 30 seconds to {url}' - _logger.error('[Slack V1] %s', error_detail, exc_info=True) + _logger.exception('[Slack V1] %s', error_detail, stack_info=True) raise Exception(error_detail) except httpx.RequestError as e: error_detail = f'Request error to {url}: {str(e)}' - _logger.error('[Slack V1] %s', error_detail, exc_info=True) - raise Exception(error_detail) + _logger.exception('[Slack V1] %s', error_detail, stack_info=True) + raise Exception(error_detail) from e # ------------------------------------------------------------------------- # Final response orchestration diff --git a/enterprise/integrations/slack/slack_view.py b/enterprise/integrations/slack/slack_view.py index 1a90a3a868..5a2c7cf476 100644 --- a/enterprise/integrations/slack/slack_view.py +++ b/enterprise/integrations/slack/slack_view.py @@ -440,13 +440,13 @@ class SlackUpdateExistingConversationView(SlackNewConversationView): response.raise_for_status() except Exception as e: - logger.error( + logger.exception( '[Slack V1] Failed to send message to conversation %s: %s', self.conversation_id, str(e), - exc_info=True, + stack_info=True, ) - raise Exception(f'Failed to send message to v1 conversation: {str(e)}') + raise Exception('Failed to send message to v1 conversation') from e async def create_or_update_conversation(self, jinja: Environment) -> str: """Send new user message to conversation.""" diff --git a/enterprise/server/app_lifespan/saas_app_lifespan_service.py b/enterprise/server/app_lifespan/saas_app_lifespan_service.py index 12d55132d9..e2527cf270 100644 --- a/enterprise/server/app_lifespan/saas_app_lifespan_service.py +++ b/enterprise/server/app_lifespan/saas_app_lifespan_service.py @@ -41,7 +41,7 @@ class SaasAppLifespanService(AppLifespanService): if svc is not None: svc.shutdown() except Exception: - logger.exception('Error shutting down analytics service') + logger.exception('Error shutting down analytics service', stack_info=True) # Release long-lived database resources: the GCP Cloud SQL connector # (background cert-refresh tasks + aiohttp ClientSession) and the @@ -52,4 +52,4 @@ class SaasAppLifespanService(AppLifespanService): await get_global_config().db_session.close() except Exception: - logger.exception('Error closing DB session injector') + logger.exception('Error closing DB session injector', stack_info=True) diff --git a/enterprise/server/auth/saas_user_auth.py b/enterprise/server/auth/saas_user_auth.py index 77dfd522ec..2bc4570684 100644 --- a/enterprise/server/auth/saas_user_auth.py +++ b/enterprise/server/auth/saas_user_auth.py @@ -153,9 +153,10 @@ class SaasUserAuth(UserAuth): try: user_uuid = UUID(self.user_id) except ValueError as exc: - logger.error( + logger.exception( 'effective_org_id_override_invalid_user_id', extra={'user_id': self.user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -266,9 +267,10 @@ class SaasUserAuth(UserAuth): user_uuid = UUID(self.user_id) except ValueError as exc: # Shouldn't happen, but treat as not-a-member. - logger.error( + logger.exception( 'x_org_id_invalid_user_id', extra={'user_id': self.user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -564,14 +566,15 @@ class SaasUserAuth(UserAuth): provider_tokens[idp_type] = ProviderToken( token=SecretStr(provider_token), user_id=None, host=host ) - except Exception as e: + except Exception: # If there was a problem with a refresh token we log and delete it - logger.error( - f'Error refreshing provider_token token: {e}', + logger.exception( + 'Error refreshing provider_token token', extra={ 'user_id': self.user_id, 'idp_type': token.identity_provider, }, + stack_info=True, ) async with a_session_maker() as session: await session.execute( @@ -713,8 +716,10 @@ class SaasUserAuth(UserAuth): except HTTPException: # Propagate validation errors raised by get_effective_org_id(). raise - except Exception as e: - logger.error(f'Error fetching org info for user {self.user_id}: {e}') + except Exception: + logger.exception( + f'Error fetching org info for user {self.user_id}', stack_info=True + ) return None @classmethod diff --git a/enterprise/server/auth/sheets_client.py b/enterprise/server/auth/sheets_client.py index baa8608bbc..914819490f 100644 --- a/enterprise/server/auth/sheets_client.py +++ b/enterprise/server/auth/sheets_client.py @@ -24,7 +24,9 @@ class GoogleSheetsClient: self.client = gspread.authorize(credentials) logger.info('Successfully initialized Google Sheets API service') except Exception: - logger.exception('Failed to initialize Google Sheets client') + logger.exception( + 'Failed to initialize Google Sheets client', stack_info=True + ) self.client = None def _get_from_cache( @@ -102,10 +104,13 @@ class GoogleSheetsClient: return usernames except gspread.exceptions.APIError: - logger.exception(f'Error accessing Google Sheet {spreadsheet_id}') + logger.exception( + f'Error accessing Google Sheet {spreadsheet_id}', stack_info=True + ) return [] except Exception: logger.exception( - f'Unexpected error accessing Google Sheet {spreadsheet_id}' + f'Unexpected error accessing Google Sheet {spreadsheet_id}', + stack_info=True, ) return [] diff --git a/enterprise/server/auth/token_manager.py b/enterprise/server/auth/token_manager.py index b2e7340bc0..0dc51a7d24 100644 --- a/enterprise/server/auth/token_manager.py +++ b/enterprise/server/auth/token_manager.py @@ -130,7 +130,7 @@ class TokenManager: return token_response['access_token'], token_response['refresh_token'] except Exception: - logger.exception('Exception when getting Keycloak tokens') + logger.exception('Exception when getting Keycloak tokens', stack_info=True) return None, None async def verify_keycloak_token( @@ -313,10 +313,11 @@ class TokenManager: return access_token except httpx.HTTPStatusError as e: # Log the full response details including the body - logger.error( + logger.exception( f'Failed to get tokens for user {user_id}, identity provider {idp} from URL {e.response.url}. ' f'Status code: {e.response.status_code}, ' - f'Response body: {e.response.text}' + f'Response body: {e.response.text}', + stack_info=True, ) raise ValueError( f'Failed to get token for user: {user_id}, identity provider: {idp}. ' @@ -571,7 +572,9 @@ class TokenManager: ) return await self.get_idp_token(tokens['access_token'], idp) except KeycloakConnectionError: - logger.exception('KeycloakConnectionError when refreshing token') + logger.exception( + 'KeycloakConnectionError when refreshing token', stack_info=True + ) raise except KeycloakPostError as e: error_message = str(e) @@ -603,9 +606,10 @@ class TokenManager: return await self.get_idp_token_from_offline_token( offline_token=offline_token, idp=idp ) - except KeycloakConnectionError as e: + except KeycloakConnectionError: logger.exception( - f'KeycloakConnectionError when getting IDP token for IDP user_id {idp_user_id}: {str(e)}' + f'KeycloakConnectionError when getting IDP token for IDP user_id {idp_user_id}', + stack_info=True, ) raise @@ -776,10 +780,14 @@ class TokenManager: return self._find_duplicate_in_users(users, base_email, current_user_id) except KeycloakConnectionError: - logger.exception('KeycloakConnectionError when checking duplicate email') + logger.exception( + 'KeycloakConnectionError when checking duplicate email', stack_info=True + ) raise - except Exception as e: - logger.exception(f'Unexpected error checking duplicate email: {e}') + except Exception: + logger.exception( + 'Unexpected error checking duplicate email', stack_info=True + ) # On any error, allow signup to proceed (fail open) return False @@ -808,7 +816,9 @@ class TokenManager: logger.info(f'Successfully deleted Keycloak user {user_id}') return True except KeycloakConnectionError: - logger.exception(f'KeycloakConnectionError when deleting user {user_id}') + logger.exception( + f'KeycloakConnectionError when deleting user {user_id}', stack_info=True + ) raise except KeycloakError as e: # User might not exist or already deleted @@ -817,8 +827,11 @@ class TokenManager: extra={'user_id': user_id, 'error': str(e)}, ) return False - except Exception as e: - logger.exception(f'Unexpected error deleting Keycloak user {user_id}: {e}') + except Exception: + logger.exception( + f'Unexpected error deleting Keycloak user {user_id}', + stack_info=True, + ) return False @retry( @@ -966,12 +979,12 @@ class TokenManager: logger.warning( f'User not found in Keycloak when attempting to disable: {user_id}' ) - except Exception as e: + except Exception: # Log error but don't raise - the caller should handle the blocking regardless email_str = f', email: {email}' if email else '' - logger.error( - f'Failed to disable Keycloak account for user_id: {user_id}{email_str}: {str(e)}', - exc_info=True, + logger.exception( + f'Failed to disable Keycloak account for user_id: {user_id}{email_str}', + stack_info=True, ) async def store_org_token(self, installation_id: int, installation_token: str): @@ -1112,5 +1125,5 @@ class TokenManager: refresh_token=refresh_token ) except Exception: - logger.exception('Exception when logging out of keycloak') + logger.exception('Exception when logging out of keycloak', stack_info=True) raise diff --git a/enterprise/server/auth/user/default_user_authorizer.py b/enterprise/server/auth/user/default_user_authorizer.py index 53f4ff553f..5a65e65681 100644 --- a/enterprise/server/auth/user/default_user_authorizer.py +++ b/enterprise/server/auth/user/default_user_authorizer.py @@ -80,7 +80,9 @@ class DefaultUserAuthorizer(UserAuthorizer): return UserAuthorizationResponse(success=True) except Exception: - logger.exception('error authorizing user', extra={'user_id': user_id}) + logger.exception( + 'error authorizing user', extra={'user_id': user_id}, stack_info=True + ) return UserAuthorizationResponse(success=False) diff --git a/enterprise/server/maintenance_task_processor/org_budget_maintenance_processor.py b/enterprise/server/maintenance_task_processor/org_budget_maintenance_processor.py index dc09946c85..5f791ca1fb 100644 --- a/enterprise/server/maintenance_task_processor/org_budget_maintenance_processor.py +++ b/enterprise/server/maintenance_task_processor/org_budget_maintenance_processor.py @@ -30,7 +30,10 @@ class OrgBudgetMaintenanceProcessor(MaintenanceTaskProcessor): except Exception as exc: logger.exception( 'org_budget_maintenance_failed', - extra={'org_id': org_id, 'error': str(exc)}, + extra={ + 'org_id': org_id, + }, + stack_info=True, ) errors.append({'org_id': org_id, 'error': str(exc)}) diff --git a/enterprise/server/middleware.py b/enterprise/server/middleware.py index 0200658452..a8b0209a71 100644 --- a/enterprise/server/middleware.py +++ b/enterprise/server/middleware.py @@ -139,7 +139,7 @@ class SetAuthCookieMiddleware: raise AuthError('Invalid authentication token') except Exception as e: logger.warning(f'JWT decode error: {str(e)}') - raise AuthError('Invalid authentication token') + raise AuthError('Invalid authentication token') from e else: # Don't fail an API call if the TOS has not been accepted. # The user will accept the TOS the next time they login. diff --git a/enterprise/server/rate_limit.py b/enterprise/server/rate_limit.py index 8b49bfffb7..a6e72ca2cd 100644 --- a/enterprise/server/rate_limit.py +++ b/enterprise/server/rate_limit.py @@ -65,14 +65,17 @@ class RateLimiter: try: allowed = await self.strategy.hit(lim, namespace, key) except Exception: - logger.exception('Rate limit check could not complete, redis issue?') + logger.exception( + 'Rate limit check could not complete, redis issue?', stack_info=True + ) if not allowed: logger.info(f'Rate limit hit for {namespace}:{key}') try: result = await self._get_stats_as_result(lim, namespace, key) except Exception: logger.exception( - 'Rate limit exceeded but window lookup failed, swallowing' + 'Rate limit exceeded but window lookup failed, swallowing', + stack_info=True, ) else: raise RateLimitException(result) diff --git a/enterprise/server/routes/agent_profiles.py b/enterprise/server/routes/agent_profiles.py index 10a24fd740..0e1fab3dcf 100644 --- a/enterprise/server/routes/agent_profiles.py +++ b/enterprise/server/routes/agent_profiles.py @@ -126,7 +126,7 @@ async def _get_org(org_id: UUID, user_id: str) -> Org: try: return await OrgService.get_org_by_id(org_id=org_id, user_id=user_id) except OrgNotFoundError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e async def _get_member( @@ -277,7 +277,7 @@ async def save_agent_profile( raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=safe_validation_error_detail(e), - ) + ) from e except Exception: # SkillValidationError / schema errors are client errors, never a # 500; stay generic — these messages can embed the input. diff --git a/enterprise/server/routes/analytics_events.py b/enterprise/server/routes/analytics_events.py index 355be1df31..8342d1d515 100644 --- a/enterprise/server/routes/analytics_events.py +++ b/enterprise/server/routes/analytics_events.py @@ -105,6 +105,7 @@ async def track_frontend_event( logger.exception( 'analytics:frontend_event:failed', extra={'event_type': body.event_type}, + stack_info=True, ) return AnalyticsEventResponse(status='ok') diff --git a/enterprise/server/routes/api_keys.py b/enterprise/server/routes/api_keys.py index 37f87116f7..8f8049571a 100644 --- a/enterprise/server/routes/api_keys.py +++ b/enterprise/server/routes/api_keys.py @@ -82,10 +82,13 @@ async def generate_byor_key(user_id: str, org_id: UUID) -> str | None: }, ) return key - except Exception as e: + except Exception: logger.exception( 'Error generating BYOR key', - extra={'user_id': user_id, 'error': str(e)}, + extra={ + 'user_id': user_id, + }, + stack_info=True, ) return None @@ -106,10 +109,13 @@ async def delete_byor_key_from_litellm( extra={'user_id': user_id}, ) return True - except Exception as e: + except Exception: logger.exception( 'Error deleting BYOR key from LiteLLM', - extra={'user_id': user_id, 'error': str(e)}, + extra={ + 'user_id': user_id, + }, + stack_info=True, ) return False @@ -216,13 +222,11 @@ async def check_byor_permitted( ) return ByorPermittedResponse(permitted=permitted) except Exception as e: - logger.exception( - 'Error checking BYOR export permission', extra={'error': str(e)} - ) + logger.exception('Error checking BYOR export permission', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to check BYOR export permission', - ) + ) from e @api_router.post('', tags=['Keys']) @@ -310,7 +314,7 @@ async def create_api_key( except HTTPException: raise except Exception: - logger.exception('Error creating API key') + logger.exception('Error creating API key', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to create API key', @@ -327,7 +331,7 @@ async def list_api_keys( keys = await api_key_store.list_api_keys(user_id, org_id=effective_org_id) return [api_key_to_response(key) for key in keys] except Exception: - logger.exception('Error listing API keys') + logger.exception('Error listing API keys', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to list API keys', @@ -369,7 +373,7 @@ async def delete_api_key( except HTTPException: raise except Exception: - logger.exception('Error deleting API key') + logger.exception('Error deleting API key', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to delete API key', @@ -492,11 +496,11 @@ async def get_llm_api_key_for_byor( # Re-raise HTTP exceptions as-is raise except Exception as e: - logger.exception('Error retrieving BYOR LLM API key', extra={'error': str(e)}) + logger.exception('Error retrieving BYOR LLM API key', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve BYOR LLM API key', - ) + ) from e @api_router.post('/llm/byor/refresh', tags=['Keys']) @@ -560,7 +564,7 @@ async def refresh_llm_api_key_for_byor( ) return LlmApiKeyResponse(key=key) except HTTPException as he: - logger.error( + logger.exception( 'HTTP exception during BYOR LLM API key refresh', extra={ 'user_id': user_id, @@ -568,6 +572,7 @@ async def refresh_llm_api_key_for_byor( 'detail': he.detail, 'exception_type': type(he).__name__, }, + stack_info=True, ) raise except Exception as e: @@ -575,11 +580,11 @@ async def refresh_llm_api_key_for_byor( 'Unexpected error refreshing BYOR LLM API key', extra={ 'user_id': user_id, - 'error': str(e), 'exception_type': type(e).__name__, }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to refresh BYOR LLM API key', - ) + ) from e diff --git a/enterprise/server/routes/auth.py b/enterprise/server/routes/auth.py index 763871e2f0..639e2e0198 100644 --- a/enterprise/server/routes/auth.py +++ b/enterprise/server/routes/auth.py @@ -178,6 +178,7 @@ async def _get_user_orgs_with_data(user_id: str, org_member_ids: list) -> list: logger.exception( 'auth:_get_user_orgs_with_data:failed', extra={'user_id': user_id, 'org_ids': [str(oid) for oid in org_member_ids]}, + stack_info=True, ) return [] @@ -217,6 +218,7 @@ async def _track_login_analytics_background( logger.exception( 'auth:identify_user:member_count_failed', extra={'user_id': user_id, 'org_id': str(org.id)}, + stack_info=True, ) member_count = None orgs_data.append( @@ -248,6 +250,7 @@ async def _track_login_analytics_background( logger.exception( 'auth:_track_login_analytics_background:failed', extra={'user_id': user_id}, + stack_info=True, ) @@ -393,8 +396,10 @@ async def keycloak_callback( error_url = f'{web_url}/login?recaptcha_blocked=true' return RedirectResponse(error_url, status_code=302) - except Exception as e: - logger.exception(f'reCAPTCHA verification error at callback: {e}') + except Exception: + logger.exception( + 'reCAPTCHA verification error at callback', stack_info=True + ) # Fail open - continue with login if reCAPTCHA service unavailable # Check email verification status @@ -571,10 +576,13 @@ async def keycloak_callback( else: redirect_url = f'{redirect_url}?email_mismatch=true' - except Exception as e: + except Exception: logger.exception( 'Unexpected error processing invitation during auth callback', - extra={'user_id': user_id, 'error': str(e)}, + extra={ + 'user_id': user_id, + }, + stack_info=True, ) # Don't fail the login if invitation processing fails if '?' in redirect_url: @@ -591,10 +599,13 @@ async def keycloak_callback( ) if accepted_invitations: user = await UserStore.get_user_by_id(user_id) or user - except Exception as e: + except Exception: logger.exception( 'Unexpected error accepting pending invitations at login', - extra={'user_id': user_id, 'error': str(e)}, + extra={ + 'user_id': user_id, + }, + stack_info=True, ) try: @@ -602,10 +613,13 @@ async def keycloak_callback( user, is_new_user=is_new_user, ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error applying default organization bootstrap', - extra={'user_id': user_id, 'error': str(e)}, + extra={ + 'user_id': user_id, + }, + stack_info=True, ) # If the user hasn't accepted the TOS, redirect to the TOS page @@ -1021,7 +1035,7 @@ async def accept_tos(request: Request): properties={'signed_up_at': datetime.now(timezone.utc).isoformat()}, ) except Exception: - logger.exception('analytics:user_signed_up:failed') + logger.exception('analytics:user_signed_up:failed', stack_info=True) # Determine final redirect - but don't override if it's the offline token flow # (the offline callback will handle post-auth redirect after storing the token) @@ -1159,7 +1173,7 @@ async def complete_onboarding( }, ) except Exception: - logger.exception('analytics:onboarding_completed:failed') + logger.exception('analytics:onboarding_completed:failed', stack_info=True) return JSONResponse( status_code=status.HTTP_200_OK, diff --git a/enterprise/server/routes/billing.py b/enterprise/server/routes/billing.py index a4b224400c..caa7a5ef42 100644 --- a/enterprise/server/routes/billing.py +++ b/enterprise/server/routes/billing.py @@ -331,7 +331,7 @@ async def success_callback(session_id: str, request: Request): credit_balance_after=new_max_budget, ) except Exception: - logger.exception('analytics:credit_purchased:failed') + logger.exception('analytics:credit_purchased:failed', stack_info=True) return RedirectResponse( f'{get_web_url(request)}/settings/billing?checkout=success', status_code=302 diff --git a/enterprise/server/routes/email.py b/enterprise/server/routes/email.py index ce4937a080..84f90b6610 100644 --- a/enterprise/server/routes/email.py +++ b/enterprise/server/routes/email.py @@ -104,13 +104,15 @@ async def update_email( except ValueError as e: # Handle validation errors from Pydantic - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) + ) from e except Exception as e: - logger.exception(f'Error updating email: {str(e)}') + logger.exception('Error updating email', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='An error occurred while updating the email', - ) + ) from e @api_router.put('/resend') diff --git a/enterprise/server/routes/integration/azure_devops.py b/enterprise/server/routes/integration/azure_devops.py index 28b4509e88..384524fc77 100644 --- a/enterprise/server/routes/integration/azure_devops.py +++ b/enterprise/server/routes/integration/azure_devops.py @@ -235,11 +235,11 @@ async def get_azure_devops_resources( except HTTPException: raise except Exception as e: - logger.exception(f'Error retrieving Azure DevOps resources: {e}') + logger.exception('Error retrieving Azure DevOps resources', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve Azure DevOps resources', - ) + ) from e @azure_devops_integration_router.post('/azure-devops/reinstall-webhook') @@ -299,11 +299,13 @@ async def reinstall_azure_devops_webhook( except HTTPException: raise except Exception as e: - logger.exception(f'Error installing Azure DevOps resolver hooks: {e}') + logger.exception( + 'Error installing Azure DevOps resolver hooks', stack_info=True + ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to install Azure DevOps resolver hooks', - ) + ) from e @azure_devops_integration_router.post('/azure-devops/uninstall-webhook') @@ -355,11 +357,13 @@ async def uninstall_azure_devops_webhook( except HTTPException: raise except Exception as e: - logger.exception(f'Error uninstalling Azure DevOps resolver hooks: {e}') + logger.exception( + 'Error uninstalling Azure DevOps resolver hooks', stack_info=True + ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to uninstall Azure DevOps resolver hooks', - ) + ) from e @azure_devops_integration_router.post('/azure-devops/events') @@ -413,6 +417,6 @@ async def azure_devops_events( ) except HTTPException: raise - except Exception as e: - logger.exception(f'Error processing Azure DevOps event: {e}') + except Exception: + logger.exception('Error processing Azure DevOps event', stack_info=True) return JSONResponse(status_code=400, content={'error': 'Invalid payload.'}) diff --git a/enterprise/server/routes/integration/bitbucket.py b/enterprise/server/routes/integration/bitbucket.py index 462135ad9a..17da81060b 100644 --- a/enterprise/server/routes/integration/bitbucket.py +++ b/enterprise/server/routes/integration/bitbucket.py @@ -121,6 +121,6 @@ async def bitbucket_events( except HTTPException: raise - except Exception as e: - logger.exception(f'Error processing Bitbucket event: {e}') + except Exception: + logger.exception('Error processing Bitbucket event', stack_info=True) return JSONResponse(status_code=400, content={'error': 'Invalid payload.'}) diff --git a/enterprise/server/routes/integration/bitbucket_dc.py b/enterprise/server/routes/integration/bitbucket_dc.py index b6de9c68a1..2148182097 100644 --- a/enterprise/server/routes/integration/bitbucket_dc.py +++ b/enterprise/server/routes/integration/bitbucket_dc.py @@ -347,11 +347,11 @@ async def get_bitbucket_dc_resources( except HTTPException: raise except Exception as e: - logger.exception(f'Error retrieving Bitbucket DC resources: {e}') + logger.exception('Error retrieving Bitbucket DC resources', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve Bitbucket DC resources', - ) + ) from e @bitbucket_dc_integration_router.post('/bitbucket-dc/enroll-webhook') @@ -392,11 +392,11 @@ async def enroll_bitbucket_dc_webhook( ) except Exception as e: - logger.exception(f'Error enrolling Bitbucket DC webhook: {e}') + logger.exception('Error enrolling Bitbucket DC webhook', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to enroll Bitbucket DC webhook', - ) + ) from e @bitbucket_dc_integration_router.patch('/bitbucket-dc/webhook-id') @@ -445,11 +445,11 @@ async def update_bitbucket_dc_webhook_id( except HTTPException: raise except Exception as e: - logger.exception(f'Error updating Bitbucket DC webhook id: {e}') + logger.exception('Error updating Bitbucket DC webhook id', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to update Bitbucket DC webhook id', - ) + ) from e @bitbucket_dc_integration_router.post('/bitbucket-dc/reinstall-webhook') @@ -526,11 +526,11 @@ async def reinstall_bitbucket_dc_webhook( except HTTPException: raise except Exception as e: - logger.exception(f'Error installing Bitbucket DC webhook: {e}') + logger.exception('Error installing Bitbucket DC webhook', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to install Bitbucket DC webhook', - ) + ) from e @bitbucket_dc_integration_router.post('/bitbucket-dc/uninstall-webhook') @@ -603,11 +603,11 @@ async def uninstall_bitbucket_dc_webhook( except HTTPException: raise except Exception as e: - logger.exception(f'Error uninstalling Bitbucket DC webhook: {e}') + logger.exception('Error uninstalling Bitbucket DC webhook', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to uninstall Bitbucket DC webhook', - ) + ) from e async def _handle_bitbucket_dc_event( @@ -711,7 +711,7 @@ async def _handle_bitbucket_dc_event( except HTTPException: raise except Exception as e: - logger.exception(f'Error processing Bitbucket DC event: {e}') + logger.exception('Error processing Bitbucket DC event', stack_info=True) # Surface the exception class name so admins reading DC's webhook # delivery UI can correlate with server logs without leaking a full # message (which may contain sensitive payload fragments). diff --git a/enterprise/server/routes/integration/github.py b/enterprise/server/routes/integration/github.py index 2143d92cc4..12e0d4535f 100644 --- a/enterprise/server/routes/integration/github.py +++ b/enterprise/server/routes/integration/github.py @@ -102,6 +102,6 @@ async def github_events( status_code=408, content={'error': 'Request timeout - client took too long to send data.'}, ) - except Exception as e: - logger.exception(f'Error processing GitHub event: {e}') + except Exception: + logger.exception('Error processing GitHub event', stack_info=True) return JSONResponse(status_code=400, content={'error': 'Invalid payload.'}) diff --git a/enterprise/server/routes/integration/gitlab.py b/enterprise/server/routes/integration/gitlab.py index 216310afb5..0e6f714fdc 100644 --- a/enterprise/server/routes/integration/gitlab.py +++ b/enterprise/server/routes/integration/gitlab.py @@ -127,8 +127,8 @@ async def gitlab_events( content={'message': 'GitLab events endpoint reached successfully.'}, ) - except Exception as e: - logger.exception(f'Error processing GitLab event: {e}') + except Exception: + logger.exception('Error processing GitLab event', stack_info=True) return JSONResponse(status_code=400, content={'error': 'Invalid payload.'}) @@ -259,11 +259,11 @@ async def get_gitlab_resources( except HTTPException: raise except Exception as e: - logger.exception(f'Error retrieving GitLab resources: {e}') + logger.exception('Error retrieving GitLab resources', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve GitLab resources', - ) + ) from e @gitlab_integration_router.post('/gitlab/reinstall-webhook') @@ -388,8 +388,8 @@ async def reinstall_gitlab_webhook( except HTTPException: raise except Exception as e: - logger.exception(f'Error reinstalling GitLab webhook: {e}') + logger.exception('Error reinstalling GitLab webhook', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to reinstall webhook', - ) + ) from e diff --git a/enterprise/server/routes/integration/jira.py b/enterprise/server/routes/integration/jira.py index fc4e2277ac..a9a2a749f1 100644 --- a/enterprise/server/routes/integration/jira.py +++ b/enterprise/server/routes/integration/jira.py @@ -327,8 +327,8 @@ async def jira_events( except HTTPException: # Re-raise HTTP exceptions (like signature verification failures) raise - except Exception as e: - logger.exception(f'Error processing Jira webhook: {e}') + except Exception: + logger.exception('Error processing Jira webhook', stack_info=True) return JSONResponse( status_code=500, content={'error': 'Internal server error processing webhook.'}, @@ -392,11 +392,11 @@ async def create_jira_workspace(request: Request, workspace_data: JiraWorkspaceC except HTTPException: raise except Exception as e: - logger.exception(f'Error creating Jira workspace: {e}') + logger.exception('Error creating Jira workspace', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to create workspace', - ) + ) from e @jira_integration_router.post('/workspaces/link') @@ -451,11 +451,11 @@ async def create_workspace_link(request: Request, link_data: JiraLinkCreate): except HTTPException: raise except Exception as e: - logger.exception(f'Error registering Jira user: {e}') + logger.exception('Error registering Jira user', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to register user', - ) + ) from e @jira_integration_router.get('/callback') @@ -655,11 +655,11 @@ async def get_current_workspace_link(request: Request): except HTTPException: raise except Exception as e: - logger.exception(f'Error retrieving Jira user: {e}') + logger.exception('Error retrieving Jira user', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve user', - ) + ) from e @jira_integration_router.post('/workspaces/unlink') @@ -707,11 +707,11 @@ async def unlink_workspace(request: Request): except HTTPException: raise except Exception as e: - logger.exception(f'Error unlinking Jira user: {e}') + logger.exception('Error unlinking Jira user', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to unlink user', - ) + ) from e @jira_integration_router.get( @@ -762,8 +762,8 @@ async def validate_workspace_integration(request: Request, workspace_name: str): except HTTPException: raise except Exception as e: - logger.exception(f'Error validating Jira organization: {e}') + logger.exception('Error validating Jira organization', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to validate organization', - ) + ) from e diff --git a/enterprise/server/routes/integration/jira_dc.py b/enterprise/server/routes/integration/jira_dc.py index b2c47c44b7..8a801414e3 100644 --- a/enterprise/server/routes/integration/jira_dc.py +++ b/enterprise/server/routes/integration/jira_dc.py @@ -463,8 +463,8 @@ async def _process_jira_dc_event( except HTTPException: # Re-raise HTTP exceptions (like signature verification failures) raise - except Exception as e: - logger.exception(f'Error processing Jira DC webhook: {e}') + except Exception: + logger.exception('Error processing Jira DC webhook', stack_info=True) return JSONResponse( status_code=500, content={'error': 'Internal server error processing webhook.'}, @@ -784,11 +784,11 @@ async def create_jira_dc_workspace( except HTTPException: raise except Exception as e: - logger.exception(f'Error creating Jira DC workspace: {e}') + logger.exception('Error creating Jira DC workspace', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to create workspace', - ) + ) from e @jira_dc_integration_router.post('/workspaces/status') @@ -820,11 +820,11 @@ async def update_jira_dc_workspace_status( except HTTPException: raise except Exception as e: - logger.exception(f'Error updating Jira DC workspace status: {e}') + logger.exception('Error updating Jira DC workspace status', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to update workspace status', - ) + ) from e @jira_dc_integration_router.post('/workspaces/link') @@ -899,11 +899,11 @@ async def create_workspace_link(request: Request, link_data: JiraDcLinkCreate): except HTTPException: raise except Exception as e: - logger.exception(f'Error registering Jira DC user: {e}') + logger.exception('Error registering Jira DC user', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to register user', - ) + ) from e @jira_dc_integration_router.get('/callback') @@ -1218,11 +1218,11 @@ async def get_current_workspace_link(request: Request): except HTTPException: raise except Exception as e: - logger.exception(f'Error retrieving Jira DC user: {e}') + logger.exception('Error retrieving Jira DC user', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve user', - ) + ) from e @jira_dc_integration_router.post('/workspaces/unlink') @@ -1297,11 +1297,11 @@ async def unlink_workspace(request: Request): except HTTPException: raise except Exception as e: - logger.exception(f'Error unlinking Jira DC user: {e}') + logger.exception('Error unlinking Jira DC user', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to unlink user', - ) + ) from e @jira_dc_integration_router.get( @@ -1346,8 +1346,8 @@ async def validate_workspace_integration(request: Request, workspace_name: str): except HTTPException: raise except Exception as e: - logger.exception(f'Error validating Jira DC workspace: {e}') + logger.exception('Error validating Jira DC workspace', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to validate workspace', - ) + ) from e diff --git a/enterprise/server/routes/integration/slack.py b/enterprise/server/routes/integration/slack.py index 61dc88e824..451c59c5be 100644 --- a/enterprise/server/routes/integration/slack.py +++ b/enterprise/server/routes/integration/slack.py @@ -137,7 +137,7 @@ async def install_callback( return RedirectResponse(auth_url) except Exception: # type: ignore - logger.error('unexpected_error', exc_info=True, stack_info=True) + logger.exception('unexpected_error', stack_info=True) return _html_response( title='Error', description='Internal server Error', @@ -442,8 +442,8 @@ async def on_options_load(request: Request, background_tasks: BackgroundTasks): extra={ 'slack_user_id': slack_user_id, 'search_value': search_value, - 'error': str(e), }, + stack_info=True, ) # Notify user about the unexpected error with error code background_tasks.add_task( diff --git a/enterprise/server/routes/oauth_device.py b/enterprise/server/routes/oauth_device.py index 22b9904662..9c86031225 100644 --- a/enterprise/server/routes/oauth_device.py +++ b/enterprise/server/routes/oauth_device.py @@ -115,7 +115,7 @@ async def device_authorization( interval=device_code_entry.current_interval, ) except Exception as e: - logger.exception('Error in device authorization: %s', str(e)) + logger.exception('Error in device authorization: %s', str(e), stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Internal server error', @@ -231,7 +231,7 @@ async def device_token(device_code: str = Form(...)): ) except Exception as e: - logger.exception('Error in device token: %s', str(e)) + logger.exception('Error in device token: %s', str(e), stack_info=True) return _oauth_error( status.HTTP_500_INTERNAL_SERVER_ERROR, 'server_error', @@ -298,7 +298,9 @@ async def device_verification_authenticated( ) except Exception as e: logger.exception( - 'Failed to create device API key after authorization: %s', str(e) + 'Failed to create device API key after authorization: %s', + str(e), + stack_info=True, ) # Clean up: revert the device authorization since API key creation failed @@ -313,12 +315,13 @@ async def device_verification_authenticated( logger.exception( 'Failed to revert device authorization during cleanup: %s', str(cleanup_error), + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to create API key for device access.', - ) + ) from e logger.info( 'Device code authorized with API key successfully', @@ -354,6 +357,7 @@ async def device_verification_authenticated( logger.exception( 'oauth_device:analytics:failed', extra={'user_id': user_id}, + stack_info=True, ) return JSONResponse( @@ -364,8 +368,8 @@ async def device_verification_authenticated( except HTTPException: raise except Exception as e: - logger.exception('Error in device verification: %s', str(e)) + logger.exception('Error in device verification: %s', str(e), stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='An unexpected error occurred. Please try again.', - ) + ) from e diff --git a/enterprise/server/routes/org_invitations.py b/enterprise/server/routes/org_invitations.py index d20cd78d47..ac373e2bde 100644 --- a/enterprise/server/routes/org_invitations.py +++ b/enterprise/server/routes/org_invitations.py @@ -139,7 +139,7 @@ async def create_invitation( role=invitation_data.role, ) except Exception: - logger.exception('analytics:team_members_invited:failed') + logger.exception('analytics:team_members_invited:failed', stack_info=True) successful_responses = [ await InvitationResponse.from_invitation(inv) for inv in successful @@ -156,21 +156,24 @@ async def create_invitation( raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e), - ) + ) from e except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), - ) + ) from e except Exception as e: logger.exception( 'Unexpected error creating batch invitations', - extra={'org_id': str(org_id), 'error': str(e)}, + extra={ + 'org_id': str(org_id), + }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='An unexpected error occurred', - ) + ) from e @invitation_router.get( @@ -203,6 +206,7 @@ async def list_pending_invitations( logger.exception( 'Error listing pending invitations', extra={'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -240,11 +244,12 @@ async def revoke_invitation( try: revoked = await OrgInvitationService.revoke_invitation(org_id, invitation_id) except InvitationInvalidError as e: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) from e except Exception: logger.exception( 'Error revoking invitation', extra={'org_id': str(org_id), 'invitation_id': invitation_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -359,7 +364,7 @@ async def accept_invitation( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='invitation_invalid', - ) + ) from e except UserAlreadyMemberError: logger.info( @@ -383,7 +388,7 @@ async def accept_invitation( raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail='email_mismatch', - ) + ) from e except Exception as e: logger.exception( @@ -391,10 +396,10 @@ async def accept_invitation( extra={ 'token_prefix': token[:10] + '...', 'user_id': user_id, - 'error': str(e), }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='An unexpected error occurred', - ) + ) from e diff --git a/enterprise/server/routes/org_profiles.py b/enterprise/server/routes/org_profiles.py index 6cc95e1843..f390184471 100644 --- a/enterprise/server/routes/org_profiles.py +++ b/enterprise/server/routes/org_profiles.py @@ -145,7 +145,7 @@ async def _get_org(org_id: UUID, user_id: str) -> Org: try: return await OrgService.get_org_by_id(org_id=org_id, user_id=user_id) except OrgNotFoundError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e @contextlib.asynccontextmanager @@ -258,7 +258,9 @@ async def save_profile( try: profiles.save(name, llm, include_secrets=request.include_secrets) except ProfileLimitExceededError as exc: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail=str(exc) + ) from exc return ProfileMutationResponse(name=name, message=f"Profile '{name}' saved") @@ -290,7 +292,9 @@ async def delete_profile( try: delete_llm_profile(agent_profiles, OrgLLMProfileMutator(profiles), name) except ProfileReferenced as exc: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail=str(exc) + ) from exc return ProfileMutationResponse(name=name, message=f"Profile '{name}' deleted") @@ -421,14 +425,18 @@ async def rename_profile( request.new_name, ) except ProfileNotFoundError as exc: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) + ) from exc except ProfileAlreadyExistsError as exc: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail=str(exc) + ) from exc except ValueError as exc: # rename_llm_profile validates new_name against PROFILE_NAME_PATTERN. raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc) - ) + ) from exc # The cascade may have repointed agent-profile refs — persist them too. after = agent_profiles.model_dump(mode='json', context={'expose_secrets': True}) if after != before: diff --git a/enterprise/server/routes/orgs.py b/enterprise/server/routes/orgs.py index e7ba3cf83d..ec3efc8d3e 100644 --- a/enterprise/server/routes/orgs.py +++ b/enterprise/server/routes/orgs.py @@ -171,10 +171,11 @@ async def list_user_orgs( current_org_id=current_org_id, ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error listing organizations', - extra={'user_id': user_id, 'error': str(e)}, + extra={'user_id': user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -235,29 +236,32 @@ async def create_org( raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=str(e), - ) - except LiteLLMIntegrationError as e: - logger.error( + ) from e + except LiteLLMIntegrationError: + logger.exception( 'LiteLLM integration failed', - extra={'user_id': user_id, 'error': str(e)}, + extra={'user_id': user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to create LiteLLM integration', ) - except OrgDatabaseError as e: - logger.error( + except OrgDatabaseError: + logger.exception( 'Database operation failed', - extra={'user_id': user_id, 'error': str(e)}, + extra={'user_id': user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to create organization', ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error creating organization', - extra={'user_id': user_id, 'error': str(e)}, + extra={'user_id': user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -281,11 +285,12 @@ async def get_org_defaults_settings( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) - except Exception as e: + ) from e + except Exception: logger.exception( 'Error getting organization defaults settings', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -330,11 +335,12 @@ async def update_org_defaults_settings( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) - except OrgDatabaseError as e: - logger.error( + ) from e + except OrgDatabaseError: + logger.exception( 'Database error updating organization defaults settings', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -342,10 +348,11 @@ async def update_org_defaults_settings( ) except HTTPException: raise - except Exception as e: + except Exception: logger.exception( 'Error updating organization defaults settings', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -373,13 +380,14 @@ async def get_legacy_org_defaults_settings( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except HTTPException: raise - except Exception as e: + except Exception: logger.exception( 'Error getting legacy organization defaults settings', - extra={'user_id': user_id, 'error': str(e)}, + extra={'user_id': user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -413,13 +421,14 @@ async def update_legacy_org_defaults_settings( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except HTTPException: raise - except Exception as e: + except Exception: logger.exception( 'Error updating legacy organization defaults settings', - extra={'user_id': user_id, 'error': str(e)}, + extra={'user_id': user_id}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -459,10 +468,10 @@ async def get_org_app_settings( status_code=status.HTTP_404_NOT_FOUND, detail='Current organization not found', ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error retrieving organization app settings', - extra={'error': str(e)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -514,7 +523,7 @@ async def update_org_app_settings( raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=str(e), - ) + ) from e except OrgNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -524,13 +533,13 @@ async def update_org_app_settings( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), - ) + ) from e except HTTPException: raise - except Exception as e: + except Exception: logger.exception( 'Unexpected error updating organization app settings', - extra={'error': str(e)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -568,11 +577,12 @@ async def get_org( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) - except Exception as e: + ) from e + except Exception: logger.exception( 'Unexpected error retrieving organization', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -626,15 +636,17 @@ async def get_me( 'org_id': str(org_id), 'role_id': e.role_id, }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='An unexpected error occurred', - ) - except Exception as e: + ) from e + except Exception: logger.exception( 'Unexpected error retrieving member details', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -711,7 +723,7 @@ async def delete_org( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except OrgAuthorizationError as e: logger.warning( 'User not authorized to delete organization', @@ -720,7 +732,7 @@ async def delete_org( raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e), - ) + ) from e except OrphanedUserError as e: logger.warning( 'Cannot delete organization: other members would be orphaned', @@ -733,20 +745,22 @@ async def delete_org( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), - ) - except OrgDatabaseError as e: - logger.error( + ) from e + except OrgDatabaseError: + logger.exception( 'Database error during organization deletion', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to delete organization', ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error during organization deletion', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -810,31 +824,33 @@ async def update_org( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except OrgNameExistsError as e: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=str(e), - ) + ) from e except PermissionError as e: # User lacks permission for LLM settings raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e), - ) - except OrgDatabaseError as e: - logger.error( + ) from e + except OrgDatabaseError: + logger.exception( 'Database operation failed', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to update organization', ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error updating organization', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -934,13 +950,13 @@ async def get_org_members( except HTTPException: raise except ValueError: - logger.exception('Invalid UUID format') + logger.exception('Invalid UUID format', stack_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid organization ID format', ) except Exception: - logger.exception('Error retrieving organization members') + logger.exception('Error retrieving organization members', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve members', @@ -994,13 +1010,13 @@ async def get_org_members_count( detail='You are not a member of this organization', ) except ValueError: - logger.exception('Invalid UUID format') + logger.exception('Invalid UUID format', stack_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid organization ID format', ) except Exception: - logger.exception('Error retrieving organization member count') + logger.exception('Error retrieving organization member count', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve member count', @@ -1093,11 +1109,12 @@ async def get_org_members_financial( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), - ) + ) from e except Exception: logger.exception( 'Error retrieving organization member financial data', extra={'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -1320,13 +1337,13 @@ async def remove_org_member( except HTTPException: raise except ValueError: - logger.exception('Invalid UUID format') + logger.exception('Invalid UUID format', stack_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid organization or user ID format', ) except Exception: - logger.exception('Error removing organization member') + logger.exception('Error removing organization member', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to remove member', @@ -1395,6 +1412,7 @@ async def switch_org( logger.exception( 'orgs:switch_org:analytics:failed', extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) # Retrieve credits from LiteLLM for the new current org @@ -1406,25 +1424,27 @@ async def switch_org( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except OrgAuthorizationError as e: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e), - ) - except OrgDatabaseError as e: - logger.error( + ) from e + except OrgDatabaseError: + logger.exception( 'Database operation failed during organization switch', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to switch organization', ) - except Exception as e: + except Exception: logger.exception( 'Unexpected error switching organization', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -1465,11 +1485,11 @@ async def update_org_member( raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail='You are not a member of this organization', - ) + ) from e raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail='Member not found in this organization', - ) + ) from e except CannotModifySelfError: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -1501,13 +1521,13 @@ async def update_org_member( detail='Failed to update member', ) except ValueError: - logger.exception('Invalid UUID format') + logger.exception('Invalid UUID format', stack_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid organization or user ID format', ) except Exception: - logger.exception('Error updating organization member') + logger.exception('Error updating organization member', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to update member', @@ -1547,7 +1567,7 @@ async def get_git_claims( for claim in claims ] except Exception: - logger.exception('Error fetching Git organization claims') + logger.exception('Error fetching Git organization claims', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to fetch Git organization claims', @@ -1616,7 +1636,7 @@ async def claim_git_organization( raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=str(e), - ) + ) from e except IntegrityError as e: # Only treat the unique constraint violation as a duplicate claim. # Other integrity errors (e.g. FK violations) should surface as 500s. @@ -1629,14 +1649,14 @@ async def claim_git_organization( git_organization=request.git_organization, ) ), - ) - logger.exception('Integrity error claiming Git organization') + ) from e + logger.exception('Integrity error claiming Git organization', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to claim Git organization', - ) + ) from e except Exception: - logger.exception('Error claiming Git organization') + logger.exception('Error claiming Git organization', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to claim Git organization', @@ -1685,7 +1705,7 @@ async def disconnect_git_organization( except HTTPException: raise except Exception: - logger.exception('Error disconnecting Git organization') + logger.exception('Error disconnecting Git organization', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to disconnect Git organization', @@ -1856,11 +1876,12 @@ async def list_org_conversations( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=e.message, - ) - except Exception as e: + ) from e + except Exception: logger.exception( 'Unexpected error listing organization conversations', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -1908,10 +1929,11 @@ async def get_org_conversation_stats( return stats - except Exception as e: + except Exception: logger.exception( 'Unexpected error getting organization conversation stats', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -1996,10 +2018,11 @@ async def get_org_conversation_usage_stats( return stats - except Exception as e: + except Exception: logger.exception( 'Unexpected error getting organization conversation usage stats', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -2069,10 +2092,11 @@ async def get_org_conversation_user_usage_stats( return stats - except Exception as e: + except Exception: logger.exception( 'Unexpected error getting organization user usage stats', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -2232,11 +2256,12 @@ async def export_org_conversations_csv( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=e.message, - ) - except Exception as e: + ) from e + except Exception: logger.exception( 'Unexpected error exporting organization conversations', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={'user_id': user_id, 'org_id': str(org_id)}, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -2288,15 +2313,15 @@ async def get_org_conversation( except HTTPException: raise - except Exception as e: + except Exception: logger.exception( 'Unexpected error getting organization conversation', extra={ 'user_id': user_id, 'org_id': str(org_id), 'conversation_id': conversation_id, - 'error': str(e), }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -2367,15 +2392,15 @@ async def stop_org_conversation( except HTTPException: raise - except Exception as e: + except Exception: logger.exception( 'Unexpected error stopping organization conversation', extra={ 'user_id': user_id, 'org_id': str(org_id), 'conversation_id': conversation_id, - 'error': str(e), }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, diff --git a/enterprise/server/routes/readiness.py b/enterprise/server/routes/readiness.py index 307182c767..e5db744910 100644 --- a/enterprise/server/routes/readiness.py +++ b/enterprise/server/routes/readiness.py @@ -15,21 +15,21 @@ async def is_ready(): async with a_session_maker() as session: await session.execute(text('SELECT 1')) except Exception as e: - logger.error(f'Database check failed: {str(e)}') + logger.exception('Database check failed', stack_info=True) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f'Database is not accessible: {str(e)}', - ) + ) from e # Check Redis connection try: redis_client = get_redis_client() redis_client.ping() except Exception as e: - logger.error(f'Redis check failed: {str(e)}') + logger.exception('Redis check failed', stack_info=True) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f'Redis cache is not accessible: {str(e)}', - ) + ) from e return 'OK' diff --git a/enterprise/server/routes/service.py b/enterprise/server/routes/service.py index 2758116458..2e9f141e15 100644 --- a/enterprise/server/routes/service.py +++ b/enterprise/server/routes/service.py @@ -185,13 +185,13 @@ async def get_or_create_api_key_for_user( extra={ 'user_id': user_id, 'org_id': str(org_id), - 'error': str(e), }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to get or create API key', - ) + ) from e logger.info( 'Service created API key for user', diff --git a/enterprise/server/routes/user_app_settings.py b/enterprise/server/routes/user_app_settings.py index 641cf1e2c4..d7a6253b5a 100644 --- a/enterprise/server/routes/user_app_settings.py +++ b/enterprise/server/routes/user_app_settings.py @@ -52,21 +52,20 @@ async def get_user_app_settings( raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e), - ) + ) from e except UserNotFoundError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except Exception as e: logger.exception( - 'Unexpected error retrieving user app settings', - extra={'error': str(e)}, + 'Unexpected error retrieving user app settings', stack_info=True ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to retrieve user app settings', - ) + ) from e @user_app_settings_router.post('/app', response_model=UserAppSettingsResponse) @@ -98,18 +97,15 @@ async def update_user_app_settings( raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e), - ) + ) from e except UserNotFoundError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), - ) + ) from e except Exception as e: - logger.exception( - 'Failed to update user app settings', - extra={'error': str(e)}, - ) + logger.exception('Failed to update user app settings', stack_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to update user app settings', - ) + ) from e diff --git a/enterprise/server/routes/user_provisioning.py b/enterprise/server/routes/user_provisioning.py index af0b44b244..d016226389 100644 --- a/enterprise/server/routes/user_provisioning.py +++ b/enterprise/server/routes/user_provisioning.py @@ -331,7 +331,7 @@ async def provision_user( raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail='Failed to create Keycloak user (it may already exist)', - ) + ) from e except Exception: logger.exception( 'provision_user:keycloak_create_unexpected', @@ -340,6 +340,7 @@ async def provision_user( 'target_org_id': str(target_org_id), 'email': email, }, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -462,8 +463,8 @@ async def provision_user( 'target_org_id': str(target_org_id), 'kc_user_id': kc_user_id, 'email': email, - 'error': str(e), }, + stack_info=True, ) await _rollback_partial_provision( token_manager=token_manager, @@ -475,7 +476,7 @@ async def provision_user( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Failed to finish provisioning user', - ) + ) from e logger.info( 'provision_user:success', @@ -565,6 +566,7 @@ async def _rollback_partial_provision( 'target_org_id': str(target_org_id), 'openhands_user_id': str(openhands_user_id), }, + stack_info=True, ) # 2. Personal-org cascade. Skip when ``UserStore.create_user`` had @@ -582,6 +584,7 @@ async def _rollback_partial_provision( 'kc_user_id': kc_user_id, 'openhands_user_id': str(openhands_user_id), }, + stack_info=True, ) # 3. Keycloak user. Always last; runs even when no OpenHands DB diff --git a/enterprise/server/services/automation_event_service.py b/enterprise/server/services/automation_event_service.py index 4d797d417c..c029705284 100644 --- a/enterprise/server/services/automation_event_service.py +++ b/enterprise/server/services/automation_event_service.py @@ -123,22 +123,22 @@ class AutomationEventService: event_payload = self._build_event_payload(org_context, payload) await self._send_to_automation_service(provider, org_id, event_payload) - except (aiohttp.ClientError, asyncio.TimeoutError) as e: + except (aiohttp.ClientError, asyncio.TimeoutError): # Network errors are expected and recoverable - logger.error( + logger.exception( f'[AutomationEventService] Network error forwarding ' - f'{provider.value} event (org_id={org_id}): {e}', - exc_info=True, + f'{provider.value} event (org_id={org_id})', extra={'installation_id': installation_id}, + stack_info=True, ) - except Exception as e: + except Exception: # Log unexpected errors. Note: This is a background task, so exceptions # won't surface to the HTTP caller - they're logged for debugging only. - logger.error( + logger.exception( f'[AutomationEventService] Unexpected error forwarding ' - f'{provider.value} event (org_id={org_id}): {e}', - exc_info=True, + f'{provider.value} event (org_id={org_id})', extra={'installation_id': installation_id}, + stack_info=True, ) # Don't re-raise in background task - just log for debugging @@ -172,19 +172,19 @@ class AutomationEventService: org_id=org_id, payload=event_payload, ) - except (aiohttp.ClientError, asyncio.TimeoutError) as e: - logger.error( + except (aiohttp.ClientError, asyncio.TimeoutError): + logger.exception( f'[AutomationEventService] Network error forwarding ' - f'jira_dc event (org_id={org_id}): {e}', - exc_info=True, + f'jira_dc event (org_id={org_id})', extra={'delivery_id': delivery_id}, + stack_info=True, ) - except Exception as e: - logger.error( + except Exception: + logger.exception( f'[AutomationEventService] Unexpected error forwarding ' - f'jira_dc event (org_id={org_id}): {e}', - exc_info=True, + f'jira_dc event (org_id={org_id})', extra={'delivery_id': delivery_id}, + stack_info=True, ) async def _resolve_org_context( @@ -632,12 +632,14 @@ class AutomationEventService: f'event to org {org_id}: {matched} automations matched' ) except asyncio.TimeoutError: - logger.error( + logger.exception( f'[AutomationEventService] Timeout ({AUTOMATION_SERVICE_TIMEOUT}s) ' - f'forwarding {source} event to automation service' + f'forwarding {source} event to automation service', + stack_info=True, ) - except aiohttp.ClientError as e: - logger.error( + except aiohttp.ClientError: + logger.exception( f'[AutomationEventService] HTTP error forwarding ' - f'{source} event to automation service: {e}' + f'{source} event to automation service', + stack_info=True, ) diff --git a/enterprise/server/services/email_service.py b/enterprise/server/services/email_service.py index 44f5fcf975..d3ef4a75e9 100644 --- a/enterprise/server/services/email_service.py +++ b/enterprise/server/services/email_service.py @@ -144,13 +144,13 @@ class EmailService: 'response_id': response.get('id') if response else None, }, ) - except Exception as e: - logger.error( + except Exception: + logger.exception( 'Failed to send invitation email', extra={ 'invitation_id': invitation_id, 'email': to_email, - 'error': str(e), }, + stack_info=True, ) raise diff --git a/enterprise/server/services/org_conversation_service.py b/enterprise/server/services/org_conversation_service.py index 7f528c6963..1c8b099953 100644 --- a/enterprise/server/services/org_conversation_service.py +++ b/enterprise/server/services/org_conversation_service.py @@ -1246,14 +1246,14 @@ class OrgConversationService: 'conversation_id': conversation_id, 'sandbox_id': metadata.sandbox_id, } - except Exception as e: + except Exception: logger.exception( 'Failed to stop sandbox', extra={ 'conversation_id': conversation_id, 'sandbox_id': metadata.sandbox_id, - 'error': str(e), }, + stack_info=True, ) return { 'success': False, diff --git a/enterprise/server/services/org_invitation_service.py b/enterprise/server/services/org_invitation_service.py index 717e971cff..8f7c7ec509 100644 --- a/enterprise/server/services/org_invitation_service.py +++ b/enterprise/server/services/org_invitation_service.py @@ -142,14 +142,14 @@ class OrgInvitationService: invitation_token=invitation.token, invitation_id=invitation.id, ) - except Exception as e: - logger.error( + except Exception: + logger.exception( 'Failed to send invitation email', extra={ 'invitation_id': invitation.id, 'email': email, - 'error': str(e), }, + stack_info=True, ) # Don't fail the invitation creation if email fails # The user can still access via direct link @@ -312,6 +312,7 @@ class OrgInvitationService: 'user_id': str(user.id), 'org_id': str(invitation.org_id), }, + stack_info=True, ) continue @@ -473,18 +474,18 @@ class OrgInvitationService: invitation.org_id, str(user_id) ) except Exception as e: - logger.error( + logger.exception( 'Failed to create LiteLLM integration for invitation acceptance', extra={ 'invitation_id': invitation.id, 'user_id': str(user_id), 'org_id': str(invitation.org_id), - 'error': str(e), }, + stack_info=True, ) raise InvitationInvalidError( 'Failed to set up organization access. Please try again.' - ) + ) from e # Step 4.5: Ensure the organization still exists before adding membership org = await OrgStore.get_org_by_id(invitation.org_id) diff --git a/enterprise/server/services/org_member_financial_service.py b/enterprise/server/services/org_member_financial_service.py index e4df3d8c84..082d8eddb7 100644 --- a/enterprise/server/services/org_member_financial_service.py +++ b/enterprise/server/services/org_member_financial_service.py @@ -74,13 +74,13 @@ class OrgMemberFinancialService: except httpx.HTTPStatusError as e: # Re-raise auth errors - these indicate configuration issues that need fixing if e.response.status_code in (401, 403): - logger.error( + logger.exception( 'LiteLLM authentication/authorization failed', extra={ 'org_id': str(org_id), 'status_code': e.response.status_code, - 'error': str(e), }, + stack_info=True, ) raise # For other HTTP errors (404, 500, etc.), use graceful degradation diff --git a/enterprise/server/services/smtp_email_service.py b/enterprise/server/services/smtp_email_service.py index 97c5a62eb6..9f8b386f9e 100644 --- a/enterprise/server/services/smtp_email_service.py +++ b/enterprise/server/services/smtp_email_service.py @@ -96,10 +96,9 @@ class SMTPEmailService: finally: client.quit() return True - except Exception as exc: - logger.error( - 'Failed to send SMTP email', - extra={'error': str(exc), **extra_payload}, + except Exception: + logger.exception( + 'Failed to send SMTP email', extra={**extra_payload}, stack_info=True ) return False diff --git a/enterprise/server/verified_models/litellm_proxy_model_router.py b/enterprise/server/verified_models/litellm_proxy_model_router.py index a037721d1f..e4cb14f755 100644 --- a/enterprise/server/verified_models/litellm_proxy_model_router.py +++ b/enterprise/server/verified_models/litellm_proxy_model_router.py @@ -257,10 +257,10 @@ class LiteLLMProxyModelService(DefaultLLMModelService): exc_info=True, ) return response - _logger.error( + _logger.exception( 'Failed to fetch models from the LiteLLM proxy and no ' 'previous result is cached; returning an empty model list', - exc_info=True, + stack_info=True, ) # Not cached, so the next request retries immediately. Still # union the catalogue so BYOK users can bring a key while the diff --git a/enterprise/server/verified_models/verified_model_router.py b/enterprise/server/verified_models/verified_model_router.py index e104c1bb73..0fc8f298c6 100644 --- a/enterprise/server/verified_models/verified_model_router.py +++ b/enterprise/server/verified_models/verified_model_router.py @@ -78,7 +78,7 @@ async def create_verified_model( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(ex), - ) + ) from ex @api_router.put('/{provider}/{model_name:path}') @@ -124,7 +124,7 @@ async def delete_verified_model( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(ex), - ) + ) from ex class SaaSLLMModelService(DefaultLLMModelService): diff --git a/enterprise/storage/org_service.py b/enterprise/storage/org_service.py index b351fac25c..55a621cbb5 100644 --- a/enterprise/storage/org_service.py +++ b/enterprise/storage/org_service.py @@ -95,9 +95,13 @@ class OrgService: except Exception as e: logger.exception( 'Error creating LiteLLM integration', - extra={'org_id': str(org_id), 'user_id': user_id, 'error': str(e)}, + extra={ + 'org_id': str(org_id), + 'user_id': user_id, + }, + stack_info=True, ) - raise LiteLLMIntegrationError(f'LiteLLM integration failed: {str(e)}') + raise LiteLLMIntegrationError('LiteLLM integration failed') from e @staticmethod def create_org_entity( @@ -289,13 +293,13 @@ class OrgService: raise except Exception as e: # Unexpected error in steps 4-6, need to clean up LiteLLM - logger.error( + logger.exception( 'Unexpected error during organization creation, initiating cleanup', extra={ 'org_id': str(org_id), 'user_id': user_id, - 'error': str(e), }, + stack_info=True, ) await OrgService._handle_failure_with_cleanup( org_id, user_id, e, 'Failed to create organization' @@ -330,13 +334,13 @@ class OrgService: return persisted_org except Exception as e: - logger.error( + logger.exception( 'Database persistence failed, initiating LiteLLM cleanup', extra={ 'org_id': str(org_id), 'user_id': user_id, - 'error': str(e), }, + stack_info=True, ) await OrgService._handle_failure_with_cleanup( org_id, user_id, e, 'Failed to create organization' @@ -378,9 +382,11 @@ class OrgService: raise OrgDatabaseError( f'{error_message}: {str(original_error)}. ' f'Cleanup also failed: {str(cleanup_error)}' - ) + ) from original_error - raise OrgDatabaseError(f'{error_message}: {str(original_error)}') + raise OrgDatabaseError( + f'{error_message}: {str(original_error)}' + ) from original_error @staticmethod async def _cleanup_litellm_resources( @@ -409,13 +415,13 @@ class OrgService: return None except Exception as e: - logger.error( + logger.exception( 'Failed to cleanup LiteLLM team (resources may be orphaned)', extra={ 'org_id': str(org_id), 'user_id': user_id, - 'error': str(e), }, + stack_info=True, ) return e @@ -596,15 +602,7 @@ class OrgService: return updated_org except Exception as e: - logger.error( - 'Failed to update organization', - extra={ - 'org_id': str(org_id), - 'user_id': user_id, - 'error': str(e), - }, - ) - raise OrgDatabaseError(f'Failed to update organization: {str(e)}') + raise OrgDatabaseError('Failed to update organization') from e @staticmethod async def get_org_credits(user_id: str, org_id: UUID) -> float | None: @@ -835,11 +833,15 @@ class OrgService: # specific failure mode and force a 500. raise except Exception as e: - logger.error( + logger.exception( 'Organization deletion failed', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + }, + stack_info=True, ) - raise OrgDatabaseError(f'Failed to delete organization: {str(e)}') + raise OrgDatabaseError('Failed to delete organization') from e @staticmethod async def check_byor_export_enabled( @@ -938,8 +940,12 @@ class OrgService: except OrgDatabaseError: raise except Exception as e: - logger.error( + logger.exception( 'Failed to switch user organization', - extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)}, + extra={ + 'user_id': user_id, + 'org_id': str(org_id), + }, + stack_info=True, ) - raise OrgDatabaseError(f'Failed to switch organization: {str(e)}') + raise OrgDatabaseError('Failed to switch organization') from e diff --git a/enterprise/storage/org_store.py b/enterprise/storage/org_store.py index 7d02e61910..f4dcb657ad 100644 --- a/enterprise/storage/org_store.py +++ b/enterprise/storage/org_store.py @@ -788,11 +788,14 @@ class OrgStore: return org - except Exception as e: + except Exception: await session.rollback() - logger.error( + logger.exception( 'Failed to delete organization - transaction rolled back', - extra={'org_id': str(org_id), 'error': str(e)}, + extra={ + 'org_id': str(org_id), + }, + stack_info=True, ) raise diff --git a/enterprise/storage/saas_settings_store.py b/enterprise/storage/saas_settings_store.py index 414559dc8c..5029d0452a 100644 --- a/enterprise/storage/saas_settings_store.py +++ b/enterprise/storage/saas_settings_store.py @@ -754,8 +754,8 @@ class SaasSettingsStore(SettingsStore): normalized.append(mp.model_dump()) return normalized return [] - except Exception as e: - logger.error(f'Error fetching org marketplaces: {e}') + except Exception: + logger.exception('Error fetching org marketplaces', stack_info=True) return [] async def _ensure_api_key( diff --git a/enterprise/sync/enrich_user_interaction_data.py b/enterprise/sync/enrich_user_interaction_data.py index e0ad56b57e..c950ed9505 100644 --- a/enterprise/sync/enrich_user_interaction_data.py +++ b/enterprise/sync/enrich_user_interaction_data.py @@ -55,9 +55,10 @@ async def main(): logger.info( f'Successfully processed PR #{pr.pr_number} from repo {pr.repo_name}' ) - except Exception as e: + except Exception: logger.exception( - f'Error processing PR #{pr.pr_number} from repo {pr.repo_name}: {str(e)}' + f'Error processing PR #{pr.pr_number} from repo {pr.repo_name}', + stack_info=True, ) logger.info('PR data enrichment process completed') diff --git a/enterprise/sync/resend_keycloak.py b/enterprise/sync/resend_keycloak.py index b7ef30531d..667d33a431 100644 --- a/enterprise/sync/resend_keycloak.py +++ b/enterprise/sync/resend_keycloak.py @@ -186,7 +186,7 @@ def get_resend_contacts(audience_id: str) -> Dict[str, Dict[str, Any]]: # efficient lookup return {contact['email'].lower(): contact for contact in contacts} except Exception: - logger.exception('Failed to get contacts from Resend') + logger.exception('Failed to get contacts from Resend', stack_info=True) raise @@ -230,7 +230,7 @@ def add_contact_to_resend( return resend.Contacts.create(params) except Exception: - logger.exception(f'Failed to add contact {email} to Resend') + logger.exception(f'Failed to add contact {email} to Resend', stack_info=True) raise @@ -309,7 +309,7 @@ def send_welcome_email( logger.info(f'Welcome email sent to {email}') return response except Exception: - logger.exception(f'Failed to send welcome email to {email}') + logger.exception(f'Failed to send welcome email to {email}', stack_info=True) raise @@ -367,7 +367,9 @@ def _backfill_existing_resend_contacts( return backfilled_count except Exception: - logger.exception('Error during backfill of existing Resend contacts') + logger.exception( + 'Error during backfill of existing Resend contacts', stack_info=True + ) # Don't fail the entire sync if backfill fails - just log and continue return 0 @@ -456,7 +458,9 @@ def sync_users_to_resend(): user_id=user.id, ) except Exception: - logger.exception(f'Failed to mark user {email} as synced') + logger.exception( + f'Failed to mark user {email} as synced', stack_info=True + ) stats['errors'] += 1 continue @@ -466,7 +470,9 @@ def sync_users_to_resend(): ) logger.info(f'Added user {email} to Resend') except Exception: - logger.exception(f'Error adding user {email} to Resend') + logger.exception( + f'Error adding user {email} to Resend', stack_info=True + ) synced_user_store.remove_synced_user(email, RESEND_AUDIENCE_ID) stats['errors'] += 1 continue @@ -482,7 +488,8 @@ def sync_users_to_resend(): except Exception: logger.exception( f'Failed to send welcome email to {email}, ' - 'but contact was added to audience' + 'but contact was added to audience', + stack_info=True, ) time.sleep(1 / RATE_LIMIT) @@ -491,10 +498,10 @@ def sync_users_to_resend(): logger.info(f'Sync completed: {stats}') except ResendAPIError: - logger.exception('Resend API error') + logger.exception('Resend API error', stack_info=True) sys.exit(1) except Exception: - logger.exception('Sync failed with unexpected error') + logger.exception('Sync failed with unexpected error', stack_info=True) sys.exit(1) diff --git a/enterprise/tests/unit/integrations/github/test_github_manager.py b/enterprise/tests/unit/integrations/github/test_github_manager.py index 4a36ccebdb..0f78cb15a8 100644 --- a/enterprise/tests/unit/integrations/github/test_github_manager.py +++ b/enterprise/tests/unit/integrations/github/test_github_manager.py @@ -698,17 +698,16 @@ class TestReceiveMessagePayloadProcessingError: expected_message = '[Github]: Error processing payload for gh interaction' - # The failure is logged at error level with exc_info so the stack trace is preserved. - error_calls = [ + # The failure is logged at exception level so the stack trace is preserved automatically. + exception_calls = [ call - for call in mock_logger.error.call_args_list + for call in mock_logger.exception.call_args_list if call.args and call.args[0] == expected_message ] - assert error_calls, ( - f'Expected error log about payload processing. ' - f'Got: {mock_logger.error.call_args_list}' + assert exception_calls, ( + f'Expected exception log about payload processing. ' + f'Got: {mock_logger.exception.call_args_list}' ) - assert all(call.kwargs.get('exc_info') is True for call in error_calls) # It must not be logged at warning level (the bug this fix addresses). warning_calls = [ diff --git a/enterprise/tests/unit/server/services/test_automation_event_service.py b/enterprise/tests/unit/server/services/test_automation_event_service.py index db8600831e..b56381c0d6 100644 --- a/enterprise/tests/unit/server/services/test_automation_event_service.py +++ b/enterprise/tests/unit/server/services/test_automation_event_service.py @@ -890,11 +890,11 @@ class TestSendToAutomationService: ProviderType.GITHUB, org_id, payload ) - mock_logger.error.assert_called() - assert 'Timeout' in str(mock_logger.error.call_args) + mock_logger.exception.assert_called() + assert 'Timeout' in str(mock_logger.exception.call_args) assert ( - 'never delivered' in str(mock_logger.error.call_args).lower() - or 'timeout' in str(mock_logger.error.call_args).lower() + 'never delivered' in str(mock_logger.exception.call_args).lower() + or 'timeout' in str(mock_logger.exception.call_args).lower() ) @pytest.mark.asyncio @@ -986,8 +986,8 @@ class TestSendToAutomationService: ProviderType.GITHUB, org_id, payload ) - mock_logger.error.assert_called() - assert 'HTTP error' in str(mock_logger.error.call_args) + mock_logger.exception.assert_called() + assert 'HTTP error' in str(mock_logger.exception.call_args) @pytest.mark.asyncio async def test_send_503_logs_error(self, mock_token_manager): diff --git a/enterprise/tests/unit/test_org_service.py b/enterprise/tests/unit/test_org_service.py index 3820152756..a2e8244d51 100644 --- a/enterprise/tests/unit/test_org_service.py +++ b/enterprise/tests/unit/test_org_service.py @@ -388,7 +388,7 @@ async def test_create_org_with_owner_database_failure_triggers_cleanup( # Verify cleanup was called assert cleanup_called - assert 'Database connection failed' in str(exc_info.value) + assert 'Database connection failed' in str(exc_info.value.__cause__) @pytest.mark.asyncio @@ -1137,7 +1137,7 @@ async def test_delete_org_with_cleanup_database_failure(session_maker, owner_rol with pytest.raises(OrgDatabaseError) as exc_info: await OrgService.delete_org_with_cleanup(user_id, org_id) - assert 'Database connection failed' in str(exc_info.value) + assert 'Database connection failed' in str(exc_info.value.__cause__) @pytest.mark.asyncio @@ -1164,7 +1164,7 @@ async def test_delete_org_with_cleanup_unexpected_none_result( with pytest.raises(OrgDatabaseError) as exc_info: await OrgService.delete_org_with_cleanup(user_id, org_id) - assert 'not found during deletion' in str(exc_info.value) + assert 'not found during deletion' in str(exc_info.value.__cause__) @pytest.mark.asyncio diff --git a/openhands/analytics/analytics_service.py b/openhands/analytics/analytics_service.py index 25cba9c659..66d2232c31 100644 --- a/openhands/analytics/analytics_service.py +++ b/openhands/analytics/analytics_service.py @@ -94,7 +94,9 @@ class AnalyticsService: properties=merged, ) except Exception: - logger.exception('AnalyticsService.capture failed for event=%s', event) + logger.exception( + 'AnalyticsService.capture failed for event=%s', event, stack_info=True + ) def set_person_properties( self, @@ -116,7 +118,9 @@ class AnalyticsService: properties=properties, ) except Exception: - logger.exception('AnalyticsService.set_person_properties failed') + logger.exception( + 'AnalyticsService.set_person_properties failed', stack_info=True + ) def group_identify( self, @@ -142,7 +146,7 @@ class AnalyticsService: distinct_id=self._distinct_id(ctx.user_id), ) except Exception: - logger.exception('AnalyticsService.group_identify failed') + logger.exception('AnalyticsService.group_identify failed', stack_info=True) # ------------------------------------------------------------------ # Typed event methods @@ -521,7 +525,7 @@ class AnalyticsService: }, ) except Exception: - logger.exception('AnalyticsService.identify_user failed') + logger.exception('AnalyticsService.identify_user failed', stack_info=True) def shutdown(self) -> None: """Flush and shut down the PostHog client. @@ -531,7 +535,7 @@ class AnalyticsService: try: self._client.shutdown() except Exception: - logger.exception('AnalyticsService.shutdown failed') + logger.exception('AnalyticsService.shutdown failed', stack_info=True) # ------------------------------------------------------------------ # Private helpers diff --git a/openhands/app_server/app_conversation/app_conversation_router.py b/openhands/app_server/app_conversation/app_conversation_router.py index fed3db1f1e..594b6bfd5b 100644 --- a/openhands/app_server/app_conversation/app_conversation_router.py +++ b/openhands/app_server/app_conversation/app_conversation_router.py @@ -401,7 +401,7 @@ async def start_app_conversation( has_repository=start_request.selected_repository is not None, ) except Exception: - logger.exception('analytics:conversation_created:failed') + logger.exception('analytics:conversation_created:failed', stack_info=True) asyncio.create_task(_consume_remaining(async_iter, db_session, httpx_client)) return result @@ -569,20 +569,21 @@ async def send_message_to_conversation( ) response.raise_for_status() except httpx.HTTPStatusError as e: - logger.error( + logger.exception( f'Agent server returned error when sending message: ' - f'{e.response.status_code} - {e.response.text}' + f'{e.response.status_code} - {e.response.text}', + stack_info=True, ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f'Agent server error: {e.response.status_code}', - ) + ) from e except httpx.RequestError as e: - logger.error(f'Failed to reach agent server: {e}') + logger.exception('Failed to reach agent server', stack_info=True) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail='Failed to reach agent server.', - ) + ) from e return AppSendMessageResponse( success=True, @@ -614,6 +615,7 @@ async def _persist_conversation_model( 'Failed to persist new llm_model on conversation %s after model ' 'switch — chip may be stale until the next refresh.', conversation_id, + stack_info=True, ) @@ -747,20 +749,23 @@ async def switch_conversation_profile( profile_llm.usage_id, ) except httpx.HTTPStatusError as e: - logger.error( + logger.exception( 'Agent server returned error during switch_llm: ' - f'{e.response.status_code} - {e.response.text}' + f'{e.response.status_code} - {e.response.text}', + stack_info=True, ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f'Agent server error: {e.response.status_code}', - ) + ) from e except httpx.RequestError as e: - logger.error(f'Failed to reach agent server during switch_llm: {e}') + logger.exception( + 'Failed to reach agent server during switch_llm', stack_info=True + ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail='Failed to reach agent server.', - ) + ) from e # Persist the new model so the chat header reflects the swap on next fetch. await _persist_conversation_model( @@ -835,9 +840,10 @@ async def switch_conversation_acp_model( request.model, ) except httpx.HTTPStatusError as e: - logger.error( + logger.exception( 'Agent server returned error during switch_acp_model: ' - f'{e.response.status_code} - {e.response.text}' + f'{e.response.status_code} - {e.response.text}', + stack_info=True, ) # Surface agent-server's 400/504 directly (not-ACP, timeout). The # pre-session 409 band-aid is gone as of SDK #3764: a pre-run switch now @@ -846,17 +852,20 @@ async def switch_conversation_acp_model( raise HTTPException( status_code=e.response.status_code, detail=f'Agent server error: {e.response.status_code}', - ) + ) from e raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f'Agent server error: {e.response.status_code}', - ) + ) from e except httpx.RequestError as e: - logger.error(f'Failed to reach agent server during switch_acp_model: {e}') + logger.exception( + 'Failed to reach agent server during switch_acp_model', + stack_info=True, + ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail='Failed to reach agent server.', - ) + ) from e # Persist so the conversation's model chip reflects the switch on next load. await _persist_conversation_model( @@ -915,7 +924,9 @@ async def _finalize_sandbox_delete( # orphaned row is left; the row + running runtime stay for the runtime-api # idle reap to capture + reap. logger.exception( - 'Deferred sandbox cleanup failed for %s; kept for retry', sandbox_id + 'Deferred sandbox cleanup failed for %s; kept for retry', + sandbox_id, + stack_info=True, ) await db_session.rollback() finally: @@ -990,7 +1001,7 @@ async def delete_app_conversation( conversation_id=conversation_id, ) except Exception: - logger.exception('analytics:conversation_deleted:failed') + logger.exception('analytics:conversation_deleted:failed', stack_info=True) # Commit the deletion await db_session.commit() @@ -1259,28 +1270,39 @@ async def _proxy_git_runtime_call( upstream.raise_for_status() return upstream.json() except httpx.HTTPStatusError as e: - logger.error( + logger.exception( 'Agent server returned error during %s: %s - %s', runtime_path, e.response.status_code, e.response.text, + stack_info=True, ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f'Agent server error: {e.response.status_code}', - ) + ) from e except (json.JSONDecodeError, httpx.DecodingError) as e: - logger.error('Agent server returned non-JSON during %s: %s', runtime_path, e) + logger.exception( + 'Agent server returned non-JSON during %s: %s', + runtime_path, + e, + stack_info=True, + ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail='Agent server returned unexpected response.', - ) + ) from e except httpx.RequestError as e: - logger.error('Failed to reach agent server during %s: %s', runtime_path, e) + logger.exception( + 'Failed to reach agent server during %s: %s', + runtime_path, + e, + stack_info=True, + ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail='Failed to reach agent server.', - ) + ) from e @router.get('/{conversation_id}/git/changes') @@ -1436,7 +1458,10 @@ async def get_conversation_skills( ) except Exception as e: - logger.error(f'Error getting skills for conversation {conversation_id}: {e}') + logger.exception( + f'Error getting skills for conversation {conversation_id}', + stack_info=True, + ) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={'error': f'Error getting skills: {str(e)}'}, @@ -1580,7 +1605,10 @@ async def get_conversation_hooks( ) except Exception as e: - logger.error(f'Error getting hooks for conversation {conversation_id}: {e}') + logger.exception( + f'Error getting hooks for conversation {conversation_id}', + stack_info=True, + ) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={'error': f'Error getting hooks: {str(e)}'}, @@ -1633,7 +1661,7 @@ async def export_conversation( conversation_id=str(conversation_id), ) except Exception: - logger.exception('analytics:trajectory_downloaded:failed') + logger.exception('analytics:trajectory_downloaded:failed', stack_info=True) return StreamingResponse( zip_stream, @@ -1643,17 +1671,17 @@ async def export_conversation( }, ) except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) + raise HTTPException(status_code=404, detail=str(e)) from e except ConversationExportAlreadyRunning as e: - raise HTTPException(status_code=409, detail=str(e)) + raise HTTPException(status_code=409, detail=str(e)) from e except ConversationExportLockUnavailable as e: - raise HTTPException(status_code=503, detail=str(e)) + raise HTTPException(status_code=503, detail=str(e)) from e except ConversationExportTooLarge as e: - raise HTTPException(status_code=413, detail=str(e)) + raise HTTPException(status_code=413, detail=str(e)) from e except Exception as e: raise HTTPException( status_code=500, detail=f'Failed to download trajectory: {str(e)}' - ) + ) from e async def _consume_remaining( diff --git a/openhands/app_server/app_conversation/live_status_app_conversation_service.py b/openhands/app_server/app_conversation/live_status_app_conversation_service.py index c1a8e8224c..e5a5f7686b 100644 --- a/openhands/app_server/app_conversation/live_status_app_conversation_service.py +++ b/openhands/app_server/app_conversation/live_status_app_conversation_service.py @@ -1018,7 +1018,9 @@ class LiveStatusAppConversationService(AppConversationServiceBase): fallback_api_key = getattr(settings_llm, 'api_key', None) except Exception: _logger.exception( - 'Failed to load profiles for sandbox %s', agent_server_url + 'Failed to load profiles for sandbox %s', + agent_server_url, + stack_info=True, ) return @@ -1344,10 +1346,10 @@ class LiveStatusAppConversationService(AppConversationServiceBase): ) mcp_servers.update(user_mcp) - except Exception as e: - _logger.error( - f'Error loading custom MCP config from user settings: {e}', - exc_info=True, + except Exception: + _logger.exception( + 'Error loading custom MCP config from user settings', + stack_info=True, ) # Continue with system config only, don't fail conversation startup _logger.warning( @@ -2487,11 +2489,11 @@ class LiveStatusAppConversationService(AppConversationServiceBase): # AppConversation extends AppConversationInfo, so we can use it directly return await self._delete_from_database(app_conversation) - except Exception as e: - _logger.error( - f'Error deleting V1 conversation {conversation_id}: {e}', + except Exception: + _logger.exception( + f'Error deleting V1 conversation {conversation_id}', extra={'conversation_id': str(conversation_id)}, - exc_info=True, + stack_info=True, ) return False diff --git a/openhands/app_server/config_api/default_llm_model_service.py b/openhands/app_server/config_api/default_llm_model_service.py index 093991aab1..3f2e71f65b 100644 --- a/openhands/app_server/config_api/default_llm_model_service.py +++ b/openhands/app_server/config_api/default_llm_model_service.py @@ -171,8 +171,8 @@ class DefaultLLMModelService(LLMModelService): resp = await client.get(ollama_url, timeout=3) ollama_models_list = resp.json()['models'] extra_models.extend('ollama/' + m['name'] for m in ollama_models_list) - except httpx.HTTPError as e: - _logger.error(f'Error getting OLLAMA models: {e}') + except httpx.HTTPError: + _logger.exception('Error getting OLLAMA models', stack_info=True) self._cached_response = get_supported_llm_models( verified_models=verified_models, diff --git a/openhands/app_server/event/aws_event_service.py b/openhands/app_server/event/aws_event_service.py index 3fd827dc48..22b3303a91 100644 --- a/openhands/app_server/event/aws_event_service.py +++ b/openhands/app_server/event/aws_event_service.py @@ -45,10 +45,10 @@ class AwsEventService(EventServiceBase): except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == 'NoSuchKey': return None - _logger.exception(f'Error reading event from {path}') + _logger.exception(f'Error reading event from {path}', stack_info=True) return None except Exception: - _logger.exception(f'Error reading event from {path}') + _logger.exception(f'Error reading event from {path}', stack_info=True) return None def _store_event(self, path: Path, event: Event): diff --git a/openhands/app_server/event/google_cloud_event_service.py b/openhands/app_server/event/google_cloud_event_service.py index 93a2b4e1ed..8991e1bc2b 100644 --- a/openhands/app_server/event/google_cloud_event_service.py +++ b/openhands/app_server/event/google_cloud_event_service.py @@ -51,7 +51,7 @@ class GoogleCloudEventService(EventServiceBase): except NotFound: return None except Exception: - _logger.exception(f'Error reading event from {path}') + _logger.exception(f'Error reading event from {path}', stack_info=True) return None def _store_event(self, path: Path, event: Event): diff --git a/openhands/app_server/event_callback/webhook_router.py b/openhands/app_server/event_callback/webhook_router.py index bd58b60317..0364da81d3 100644 --- a/openhands/app_server/event_callback/webhook_router.py +++ b/openhands/app_server/event_callback/webhook_router.py @@ -529,7 +529,9 @@ async def on_event( conversation_id, app_conversation_info, events, exec_status ) except Exception: - _logger.exception('analytics:conversation_terminal:failed') + _logger.exception( + 'analytics:conversation_terminal:failed', stack_info=True + ) background_tasks.add_task( _run_callbacks_in_bg_and_close, @@ -633,8 +635,11 @@ def _import_all_tools(): if is_pkg: # Check if it's a subpackage try: importlib.import_module(name) - except ImportError as e: - _logger.error(f"Warning: Could not import subpackage '{name}': {e}") + except ImportError: + _logger.exception( + f"Warning: Could not import subpackage '{name}'", + stack_info=True, + ) _import_all_tools() diff --git a/openhands/app_server/file_store/google_cloud.py b/openhands/app_server/file_store/google_cloud.py index 882a0ee5f5..a578a695e8 100644 --- a/openhands/app_server/file_store/google_cloud.py +++ b/openhands/app_server/file_store/google_cloud.py @@ -61,7 +61,7 @@ class GoogleCloudFileStore(FileStore): with blob.open('r') as f: return str(f.read()) except NotFound as err: - raise FileNotFoundError(err) + raise FileNotFoundError(err) from err def list(self, path: str) -> list[str]: if not path or path == '/': diff --git a/openhands/app_server/file_store/local.py b/openhands/app_server/file_store/local.py index bdcafd9550..10794c64d6 100644 --- a/openhands/app_server/file_store/local.py +++ b/openhands/app_server/file_store/local.py @@ -79,5 +79,5 @@ class LocalFileStore(FileStore): elif os.path.isdir(full_path): shutil.rmtree(full_path) logger.debug(f'Removed local directory: {full_path}') - except Exception as e: - logger.error(f'Error clearing local file store: {str(e)}') + except Exception: + logger.exception('Error clearing local file store', stack_info=True) diff --git a/openhands/app_server/file_store/memory.py b/openhands/app_server/file_store/memory.py index b6e0530e6a..c0eadacf09 100644 --- a/openhands/app_server/file_store/memory.py +++ b/openhands/app_server/file_store/memory.py @@ -48,5 +48,5 @@ class InMemoryFileStore(FileStore): for key in keys_to_delete: del self.files[key] logger.debug(f'Cleared in-memory file store: {path}') - except Exception as e: - logger.error(f'Error clearing in-memory file store: {str(e)}') + except Exception: + logger.exception('Error clearing in-memory file store', stack_info=True) diff --git a/openhands/app_server/file_store/s3.py b/openhands/app_server/file_store/s3.py index 561e307918..1562b913da 100644 --- a/openhands/app_server/file_store/s3.py +++ b/openhands/app_server/file_store/s3.py @@ -66,14 +66,14 @@ class S3FileStore(FileStore): if e.response['Error']['Code'] == 'AccessDenied': raise FileNotFoundError( f"Error: Access denied to bucket '{self._get_bucket_name()}'." - ) + ) from e elif e.response['Error']['Code'] == 'NoSuchBucket': raise FileNotFoundError( f"Error: The bucket '{self._get_bucket_name()}' does not exist." - ) + ) from e raise FileNotFoundError( - f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}: {e}" - ) + f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}" + ) from e def write_from_path(self, path: str, source_path: str) -> None: # upload_file streams the file from disk in parts; never buffers the whole @@ -84,14 +84,14 @@ class S3FileStore(FileStore): if e.response['Error']['Code'] == 'AccessDenied': raise FileNotFoundError( f"Error: Access denied to bucket '{self._get_bucket_name()}'." - ) + ) from e elif e.response['Error']['Code'] == 'NoSuchBucket': raise FileNotFoundError( f"Error: The bucket '{self._get_bucket_name()}' does not exist." - ) + ) from e raise FileNotFoundError( - f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}: {e}" - ) + f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}" + ) from e def read(self, path: str) -> str: try: @@ -105,19 +105,19 @@ class S3FileStore(FileStore): if e.response['Error']['Code'] == 'NoSuchBucket': raise FileNotFoundError( f"Error: The bucket '{self._get_bucket_name()}' does not exist." - ) + ) from e elif e.response['Error']['Code'] == 'NoSuchKey': raise FileNotFoundError( f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'." - ) + ) from e else: raise FileNotFoundError( - f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}" - ) + f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}" + ) from e except Exception as e: raise FileNotFoundError( - f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}" - ) + f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}" + ) from e def list(self, path: str) -> list[str]: if not path or path == '/': @@ -175,23 +175,23 @@ class S3FileStore(FileStore): if e.response['Error']['Code'] == 'NoSuchBucket': raise FileNotFoundError( f"Error: The bucket '{self._get_bucket_name()}' does not exist." - ) + ) from e elif e.response['Error']['Code'] == 'AccessDenied': raise FileNotFoundError( f"Error: Access denied to bucket '{self._get_bucket_name()}'." - ) + ) from e elif e.response['Error']['Code'] == 'NoSuchKey': raise FileNotFoundError( f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'." - ) + ) from e else: raise FileNotFoundError( - f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}': {e}" - ) + f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}'" + ) from e except Exception as e: raise FileNotFoundError( - f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}: {e}" - ) + f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}" + ) from e def _ensure_url_scheme(self, secure: bool, url: str | None) -> str | None: if not url: diff --git a/openhands/app_server/integrations/azure_devops/azure_devops_service.py b/openhands/app_server/integrations/azure_devops/azure_devops_service.py index f12e0511f5..9ad6841a54 100644 --- a/openhands/app_server/integrations/azure_devops/azure_devops_service.py +++ b/openhands/app_server/integrations/azure_devops/azure_devops_service.py @@ -212,9 +212,9 @@ class AzureDevOpsService( return response.json(), headers except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e def _parse_repository(self, repository: str) -> tuple[str, str, str]: """Parse repository string into organization, project, and repo name. diff --git a/openhands/app_server/integrations/azure_devops/service/resolver.py b/openhands/app_server/integrations/azure_devops/service/resolver.py index 99cad98132..5bfd6bc696 100644 --- a/openhands/app_server/integrations/azure_devops/service/resolver.py +++ b/openhands/app_server/integrations/azure_devops/service/resolver.py @@ -51,7 +51,7 @@ class AzureDevOpsResolverMixin(AzureDevOpsMixinBase): body = fields.get('System.Description') or '' return title, body except Exception as wi_error: - logger.error(f'Failed to get as work item: {wi_error}') + logger.exception(f'Failed to get as work item: {wi_error}', stack_info=True) return '', '' async def get_issue_or_pr_comments( @@ -86,7 +86,9 @@ class AzureDevOpsResolverMixin(AzureDevOpsMixinBase): repository, issue_number, max_comments ) except Exception as wi_error: - logger.error(f'Failed to get work item comments: {wi_error}') + logger.exception( + f'Failed to get work item comments: {wi_error}', stack_info=True + ) return [] async def get_review_thread_comments( @@ -164,5 +166,7 @@ class AzureDevOpsResolverMixin(AzureDevOpsMixinBase): return all_comments[:max_comments] except Exception as error: - logger.error(f'Failed to get thread {thread_id} comments: {error}') + logger.exception( + f'Failed to get thread {thread_id} comments: {error}', stack_info=True + ) return [] diff --git a/openhands/app_server/integrations/bitbucket/service/base.py b/openhands/app_server/integrations/bitbucket/service/base.py index 93fc70a3f6..e29d7e8cb1 100644 --- a/openhands/app_server/integrations/bitbucket/service/base.py +++ b/openhands/app_server/integrations/bitbucket/service/base.py @@ -126,9 +126,9 @@ class BitBucketMixinBase(BaseGitService, HTTPClient): response.raise_for_status() return response.json(), dict(response.headers) except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e async def _fetch_paginated_data( self, url: str, params: dict, max_items: int diff --git a/openhands/app_server/integrations/bitbucket_data_center/service/base.py b/openhands/app_server/integrations/bitbucket_data_center/service/base.py index 0ce6eaa897..19b38eb4ca 100644 --- a/openhands/app_server/integrations/bitbucket_data_center/service/base.py +++ b/openhands/app_server/integrations/bitbucket_data_center/service/base.py @@ -139,9 +139,9 @@ class BitbucketDCMixinBase(BaseGitService, HTTPClient): data = response.text return data, dict(response.headers) except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e async def verify_access(self) -> None: """Verify that the token and host are valid by making a lightweight API call. diff --git a/openhands/app_server/integrations/forgejo/service/base.py b/openhands/app_server/integrations/forgejo/service/base.py index e420814a92..e131c2cbe6 100644 --- a/openhands/app_server/integrations/forgejo/service/base.py +++ b/openhands/app_server/integrations/forgejo/service/base.py @@ -119,9 +119,9 @@ class ForgejoMixinBase(BaseGitService, HTTPClient): return response.text, headers_out except httpx.HTTPStatusError as err: - raise self.handle_http_status_error(err) + raise self.handle_http_status_error(err) from err except httpx.HTTPError as err: - raise self.handle_http_error(err) + raise self.handle_http_error(err) from err def _resolve_base_url( self, diff --git a/openhands/app_server/integrations/github/service/base.py b/openhands/app_server/integrations/github/service/base.py index e6ddba41f7..ca703350f6 100644 --- a/openhands/app_server/integrations/github/service/base.py +++ b/openhands/app_server/integrations/github/service/base.py @@ -90,9 +90,9 @@ class GitHubMixinBase(BaseGitService, HTTPClient): return response.json(), headers except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e async def execute_graphql_query( self, query: str, variables: dict[str, Any] @@ -117,9 +117,9 @@ class GitHubMixinBase(BaseGitService, HTTPClient): return dict(result) except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e async def get_user_emails(self) -> list[dict]: """Fetch the authenticated user's email addresses from GitHub. diff --git a/openhands/app_server/integrations/gitlab/service/base.py b/openhands/app_server/integrations/gitlab/service/base.py index 5f727ae577..dbbe4fc509 100644 --- a/openhands/app_server/integrations/gitlab/service/base.py +++ b/openhands/app_server/integrations/gitlab/service/base.py @@ -81,9 +81,9 @@ class GitLabMixinBase(BaseGitService, HTTPClient): return response.text, headers except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e async def execute_graphql_query( self, query: str, variables: dict[str, Any] | None = None @@ -134,9 +134,9 @@ class GitLabMixinBase(BaseGitService, HTTPClient): return result.get('data') except httpx.HTTPStatusError as e: - raise self.handle_http_status_error(e) + raise self.handle_http_status_error(e) from e except httpx.HTTPError as e: - raise self.handle_http_error(e) + raise self.handle_http_error(e) from e async def get_user(self) -> User: url = f'{self.BASE_URL}/user' diff --git a/openhands/app_server/integrations/provider.py b/openhands/app_server/integrations/provider.py index 9509120552..c6dd51a802 100644 --- a/openhands/app_server/integrations/provider.py +++ b/openhands/app_server/integrations/provider.py @@ -216,10 +216,10 @@ class ProviderHandler: data = TokenResponse.model_validate_json(resp.text) return SecretStr(data.token) - except Exception as e: - logger.error( - f'Failed to fetch latest token for provider {provider}: {e}', - exc_info=True, + except Exception: + logger.exception( + f'Failed to fetch latest token for provider {provider}', + stack_info=True, ) return None diff --git a/openhands/app_server/mcp/mcp_router.py b/openhands/app_server/mcp/mcp_router.py index 3a715de560..2c61c6fdf2 100644 --- a/openhands/app_server/mcp/mcp_router.py +++ b/openhands/app_server/mcp/mcp_router.py @@ -71,8 +71,8 @@ def init_tavily_proxy() -> None: # Mount under 'tavily' namespace so tools are accessible as tavily_* mcp_server.mount(namespace='tavily', server=proxy_server) logger.info('Tavily MCP proxy initialized successfully') - except Exception as e: - logger.error(f'Failed to initialize Tavily MCP proxy: {e}') + except Exception: + logger.exception('Failed to initialize Tavily MCP proxy', stack_info=True) async def get_conversation_link( @@ -207,7 +207,7 @@ async def create_pr( except Exception as e: error = f'Error creating pull request: {e}' - raise ToolError(str(error)) + raise ToolError(str(error)) from e return response @@ -281,7 +281,7 @@ async def create_mr( except Exception as e: error = f'Error creating merge request: {e}' - raise ToolError(str(error)) + raise ToolError(str(error)) from e return response @@ -347,8 +347,8 @@ async def create_bitbucket_pr( except Exception as e: error = f'Error creating pull request: {e}' - logger.error(error) - raise ToolError(str(error)) + logger.exception(error, stack_info=True) + raise ToolError(str(error)) from e return response @@ -414,8 +414,8 @@ async def create_bitbucket_data_center_pr( except Exception as e: error = f'Error creating pull request: {e}' - logger.error(error) - raise ToolError(str(error)) + logger.exception(error, stack_info=True) + raise ToolError(str(error)) from e return response @@ -481,7 +481,7 @@ async def create_azure_devops_pr( except Exception as e: error = f'Error creating pull request: {e}' - logger.error(error) - raise ToolError(str(error)) + logger.exception(error, stack_info=True) + raise ToolError(str(error)) from e return response diff --git a/openhands/app_server/pending_messages/pending_message_router.py b/openhands/app_server/pending_messages/pending_message_router.py index 78e9f7a6ce..73b0f7f70a 100644 --- a/openhands/app_server/pending_messages/pending_message_router.py +++ b/openhands/app_server/pending_messages/pending_message_router.py @@ -80,7 +80,7 @@ async def queue_pending_message( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f'Invalid content format: {e}', - ) + ) from e # Rate limit: max 10 pending messages per conversation pending_count = await pending_service.count_pending_messages(conversation_id) diff --git a/openhands/app_server/sandbox/docker_sandbox_service.py b/openhands/app_server/sandbox/docker_sandbox_service.py index 63fc7387f6..3b741bf179 100644 --- a/openhands/app_server/sandbox/docker_sandbox_service.py +++ b/openhands/app_server/sandbox/docker_sandbox_service.py @@ -513,7 +513,7 @@ class DockerSandboxService(SandboxService): return sandbox_info except APIError as e: - raise SandboxError(f'Failed to start container: {e}') + raise SandboxError('Failed to start container') from e async def resume_sandbox(self, sandbox_id: str) -> bool: """Resume a paused sandbox.""" diff --git a/openhands/app_server/sandbox/process_sandbox_service.py b/openhands/app_server/sandbox/process_sandbox_service.py index 59fdf7207b..b5f440ac92 100644 --- a/openhands/app_server/sandbox/process_sandbox_service.py +++ b/openhands/app_server/sandbox/process_sandbox_service.py @@ -160,7 +160,7 @@ class ProcessSandboxService(SandboxService): return process except Exception as e: - raise SandboxError(f'Failed to start agent process: {e}') + raise SandboxError('Failed to start agent process') from e async def _wait_for_server_ready(self, port: int, timeout: int = 30) -> bool: """Wait for the agent server to be ready.""" diff --git a/openhands/app_server/sandbox/remote_sandbox_service.py b/openhands/app_server/sandbox/remote_sandbox_service.py index 27b10461a6..a1f5b3dbd9 100644 --- a/openhands/app_server/sandbox/remote_sandbox_service.py +++ b/openhands/app_server/sandbox/remote_sandbox_service.py @@ -143,10 +143,13 @@ class RemoteSandboxService(SandboxService): if attempt + 1 < attempts: _logger.warning(f'Timeout for URL {url}; retrying') continue - _logger.error(f'No response received within timeout for URL: {url}') + _logger.exception( + f'No response received within timeout for URL: {url}', + stack_info=True, + ) raise - except httpx.HTTPError as e: - _logger.error(f'HTTP error for URL {url}: {e}') + except httpx.HTTPError: + _logger.exception(f'HTTP error for URL {url}', stack_info=True) raise raise last_exc # type: ignore[misc] # unreachable; keeps mypy happy @@ -510,8 +513,8 @@ class RemoteSandboxService(SandboxService): return self._to_sandbox_info(stored_sandbox, runtime_data) except httpx.HTTPError as e: - _logger.error(f'Failed to start sandbox: {e}') - raise SandboxError(f'Failed to start sandbox: {e}') + _logger.exception('Failed to start sandbox', stack_info=True) + raise SandboxError('Failed to start sandbox') from e async def resume_sandbox(self, sandbox_id: str) -> bool: """Resume a paused sandbox. @@ -550,8 +553,8 @@ class RemoteSandboxService(SandboxService): ) return True - except httpx.HTTPError as e: - _logger.error(f'Error resuming sandbox {sandbox_id}: {e}') + except httpx.HTTPError: + _logger.exception(f'Error resuming sandbox {sandbox_id}', stack_info=True) return False async def pause_sandbox(self, sandbox_id: str) -> bool: @@ -580,8 +583,8 @@ class RemoteSandboxService(SandboxService): response.raise_for_status() return True - except httpx.HTTPError as e: - _logger.error(f'Error pausing sandbox {sandbox_id}: {e}') + except httpx.HTTPError: + _logger.exception(f'Error pausing sandbox {sandbox_id}', stack_info=True) return False async def delete_sandbox(self, sandbox_id: str) -> bool: @@ -644,7 +647,7 @@ class RemoteSandboxService(SandboxService): # signal retryable (503) — never a 404. Persist the key invalidation # now: the caller rolls back on this raise, which would otherwise # restore the hash and leave a just-revoked key valid. - _logger.error(f'Error deleting sandbox {sandbox_id}: {e}') + _logger.exception(f'Error deleting sandbox {sandbox_id}', stack_info=True) if had_key: await self.db_session.commit() raise SandboxDeleteRetryError( @@ -718,7 +721,9 @@ class RemoteSandboxService(SandboxService): # Could not resolve the workspace layout: never archive to the wrong # path. Honor REQUIRED (block + retry) vs best-effort (proceed). _logger.exception( - 'Could not resolve archive path for %s', stored_sandbox.id + 'Could not resolve archive path for %s', + stored_sandbox.id, + stack_info=True, ) return not workspace_archive.archive_required() @@ -764,6 +769,7 @@ class RemoteSandboxService(SandboxService): 'Workspace archive lookup failed for %s (%s)', sandbox_id, conversation_id, + stack_info=True, ) return not workspace_archive.archive_required() except Exception: @@ -771,6 +777,7 @@ class RemoteSandboxService(SandboxService): 'Workspace archive lookup failed for %s (%s)', sandbox_id, conversation_id, + stack_info=True, ) return not workspace_archive.archive_required() archived = await self._archive_workspace( diff --git a/openhands/app_server/sandbox/workspace_archive.py b/openhands/app_server/sandbox/workspace_archive.py index 5177e8946f..bcd5a54799 100644 --- a/openhands/app_server/sandbox/workspace_archive.py +++ b/openhands/app_server/sandbox/workspace_archive.py @@ -525,7 +525,11 @@ async def archive_workspace( ) except Exception as e: _logger.exception( - 'Workspace archive upload (%s) failed for %s: %s', fmt, sandbox_id, e + 'Workspace archive upload (%s) failed for %s: %s', + fmt, + sandbox_id, + e, + stack_info=True, ) retryable_failure = True finally: diff --git a/openhands/app_server/services/db_session_injector.py b/openhands/app_server/services/db_session_injector.py index a6ce22f043..18ebdfedc2 100644 --- a/openhands/app_server/services/db_session_injector.py +++ b/openhands/app_server/services/db_session_injector.py @@ -364,7 +364,9 @@ class DbSessionInjector(BaseModel, Injector[AsyncSession]): if asyncio.iscoroutine(result): await result except Exception: - _logger.exception('Error closing GCP Cloud SQL connector') + _logger.exception( + 'Error closing GCP Cloud SQL connector', stack_info=True + ) self._gcp_connector = None engine = self._async_engine @@ -372,7 +374,7 @@ class DbSessionInjector(BaseModel, Injector[AsyncSession]): try: await engine.dispose() except Exception: - _logger.exception('Error disposing async DB engine') + _logger.exception('Error disposing async DB engine', stack_info=True) def set_db_session_keep_open(state: InjectorState, keep_open: bool): diff --git a/openhands/app_server/services/jwt_service.py b/openhands/app_server/services/jwt_service.py index 0bea35a063..fbb9cd8c75 100644 --- a/openhands/app_server/services/jwt_service.py +++ b/openhands/app_server/services/jwt_service.py @@ -142,7 +142,7 @@ class JwtService: payload = jwt.decode(token, secret_key, algorithms=['HS256']) return payload except jwt.InvalidTokenError as e: - raise jwt.InvalidTokenError(f'Token verification failed: {str(e)}') + raise jwt.InvalidTokenError('Token verification failed') from e def create_jwe_token( self, @@ -244,7 +244,7 @@ class JwtService: raise ValueError('Decryption produced no plaintext') return json.loads(result.plaintext) except Exception as e: - raise Exception(f'Token decryption failed: {str(e)}') + raise Exception('Token decryption failed') from e # ------------------------------------------------------------------ # Symmetric encrypt / decrypt helpers (JWE with legacy Fernet fallback) diff --git a/openhands/app_server/settings/settings_router.py b/openhands/app_server/settings/settings_router.py index 07e1d12f2b..fb10d32bb3 100644 --- a/openhands/app_server/settings/settings_router.py +++ b/openhands/app_server/settings/settings_router.py @@ -321,7 +321,7 @@ async def store_settings( settings_changed=settings_changed, ) except Exception: - logger.exception('analytics:settings_saved:failed') + logger.exception('analytics:settings_saved:failed', stack_info=True) return JSONResponse( status_code=status.HTTP_200_OK, diff --git a/openhands/app_server/user/skills_router.py b/openhands/app_server/user/skills_router.py index bb632422d9..f5208b3015 100644 --- a/openhands/app_server/user/skills_router.py +++ b/openhands/app_server/user/skills_router.py @@ -430,13 +430,15 @@ async def get_marketplace_skills( marketplace_skills[marketplace.name] = skill_names except Exception as e: - logger.exception(f'Unexpected error in marketplace-skills endpoint: {e}') + logger.exception( + 'Unexpected error in marketplace-skills endpoint', stack_info=True + ) errors.append(f'Internal error: {str(e)}') # Clean up before raising for clone_dir in cloned_dirs: _cleanup_clone_dir(clone_dir) # Raise HTTP 500 for critical errors - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e finally: # Clean up cloned directories for clone_dir in cloned_dirs: diff --git a/openhands/app_server/utils/llm.py b/openhands/app_server/utils/llm.py index 324c857392..6d73c34551 100644 --- a/openhands/app_server/utils/llm.py +++ b/openhands/app_server/utils/llm.py @@ -154,8 +154,11 @@ def resolve_llm_base_url( return managed_proxy_url try: return get_provider_api_base(model) - except Exception as e: - logger.error(f'Failed to get api_base from litellm for model {model}: {e}') + except Exception: + logger.exception( + f'Failed to get api_base from litellm for model {model}', + stack_info=True, + ) return None diff --git a/openhands/app_server/utils/shutdown_listener.py b/openhands/app_server/utils/shutdown_listener.py index e76b8f55fa..ccc4ba9440 100644 --- a/openhands/app_server/utils/shutdown_listener.py +++ b/openhands/app_server/utils/shutdown_listener.py @@ -30,7 +30,7 @@ def _register_signal_handler(sig: signal.Signals) -> None: try: callable() except Exception: - logger.exception('Error calling shutdown listener') + logger.exception('Error calling shutdown listener', stack_info=True) if original_handler: original_handler(sig_, frame) # type: ignore[unreachable] diff --git a/tests/unit/server/routes/test_mcp_routes.py b/tests/unit/server/routes/test_mcp_routes.py index db97a99d1e..c799ba886b 100644 --- a/tests/unit/server/routes/test_mcp_routes.py +++ b/tests/unit/server/routes/test_mcp_routes.py @@ -280,8 +280,8 @@ class TestInitTavilyProxy: init_tavily_proxy() # Verify error was logged - mock_logger.error.assert_called_once_with( - 'Failed to initialize Tavily MCP proxy: Connection failed' + mock_logger.exception.assert_called_once_with( + 'Failed to initialize Tavily MCP proxy', stack_info=True ) # Verify mount was not called