Configuração de Logs e Tratamento Global de Exceções¶
Esta seção detalha a implementação de uma estratégia robusta de logs e um tratamento global de exceções para a API. Essas funcionalidades são cruciais para a observabilidade, depuração e estabilidade do projeto em produção.
Uma boa estratégia de logging permite monitorar o comportamento da aplicação, identificar problemas e diagnosticar erros em tempo real. O tratamento global de exceções garante que erros inesperados sejam capturados, registrados e apresentados ao cliente de forma consistente e segura, sem expor detalhes internos sensíveis.
A implementação envolverá as seguintes alterações:
Configuração de Logs: Definir uma configuração avançada de logging no settings.py do Django, com diferentes níveis, handlers (console, arquivo) e formatadores.
Middleware de Tratamento de Exceções: Criar um middleware personalizado para capturar e registrar exceções não tratadas na camada de apresentação (API), retornando respostas de erro padronizadas.
Integração do Middleware: Adicionar o novo middleware à configuração do Django.
Uso de Logs: Exemplos de como utilizar o logger em diferentes camadas para registrar eventos e erros específicos.
Testes de Integração: Adicionar testes para validar o comportamento do tratamento global de exceções na API.
3. Configuração de Logs (project/project/settings.py)¶
Configuramos o sistema de logging do Django para incluir múltiplos handlers (console, arquivo, arquivo de erros), formatadores detalhados e loggers específicos para as aplicações django, core e project. Isso permite um controle granular sobre como e onde as mensagens de log são registradas.
Criamos um handler de exceções customizado que se integra ao Django REST Framework. Ele captura exceções, registra-as usando o sistema de logging, e retorna uma resposta padronizada ao cliente, incluindo detalhes do traceback em ambiente de DEBUG.
# project/core/middleware/custom_exception_middleware.pyimportloggingimporttracebackfromdjango.confimportsettingsfromrest_frameworkimportstatusfromrest_framework.responseimportResponsefromrest_framework.viewsimportexception_handlerlogger=logging.getLogger(__name__)defcustom_exception_handler(exc,context):response=exception_handler(exc,context)ifresponseisnotNone:ifnotresponse.data.get("status_code"):response.data["status_code"]=response.status_codeifisinstance(exc,ValueError):response.data["detail"]=str(exc)response.status_code=status.HTTP_400_BAD_REQUESTifsettings.DEBUG:response.data["traceback"]=traceback.format_exc()ifresponse.status_code>=500:logger.exception("Erro de servidor interno: %s",exc)elifresponse.status_code>=400:logger.warning("Erro de cliente: %s",exc)else:logger.exception("Erro inesperado: %s",exc)response=Response({"detail":"Ocorreu um erro inesperado.","status_code":500},status=status.HTTP_500_INTERNAL_SERVER_ERROR,)ifsettings.DEBUG:response.data["traceback"]=traceback.format_exc()returnresponse
5. Integração do Middleware (project/project/settings.py)¶
Para ativar o middleware e o handler de exceções customizado, fizemos as seguintes alterações no settings.py:
Adicionamos o CustomExceptionMiddleware à lista MIDDLEWARE.
Configuramos o REST_FRAMEWORK para usar o custom_exception_handler.
# project/project/settings.py# ... outras configurações ...MIDDLEWARE=["django.middleware.security.SecurityMiddleware","django.contrib.sessions.middleware.SessionMiddleware","django.middleware.common.CommonMiddleware","django.middleware.csrf.CsrfViewMiddleware","django.contrib.auth.middleware.AuthenticationMiddleware","django.contrib.messages.middleware.MessageMiddleware","django.middleware.clickjacking.XFrameOptionsMiddleware","core.middleware.custom_exception_middleware.CustomExceptionMiddleware",# Adicionado middleware de exceção customizado]# ... outras configurações ...REST_FRAMEWORK={# ... outras configurações do DRF ..."EXCEPTION_HANDLER":"core.middleware.custom_exception_middleware.custom_exception_handler",# Handler de exceção customizado}# ... outras configurações ...
# project/core/domain/use_cases/user_use_cases.pyimportlogging# ... outras importações ...logger=logging.getLogger(__name__)classLoginUserUseCase:# ... __init__ ...defexecute(self,request:LoginUserRequest)->LoginUserResponse:logger.info("Attempting to log in user with email: %s",request.email)user=self.user_repository.get_user_by_email(request.email)ifnotuser:logger.warning("Login failed: User not found for email %s",request.email)raiseValueError("Invalid credentials")ifnotself.auth_gateway.check_password(user.id,request.password):logger.warning("Login failed: Invalid password for user ID %s",user.id)raiseValueError("Invalid credentials")access_token,refresh_token=self.auth_gateway.create_tokens(user.id)logger.info("User %s logged in successfully. User ID: %s",request.email,user.id)returnLoginUserResponse(id=user.id,email=user.email,access_token=access_token,refresh_token=refresh_token,)
# project/core/repositories/user_repository_impl.pyimportlogging# ... outras importações ...logger=logging.getLogger(__name__)classDjangoUserRepository(UserRepository):# ... outros métodos ...defget_by_id(self,user_id:str)->Optional[DomainUser]:try:user=DjangoUser.objects.get(id=user_id)logger.debug("User found by ID: %s",user_id)returnself._to_domain_user(user)exceptDjangoUser.DoesNotExist:logger.warning("User not found by ID: %s",user_id)returnNonedefcreate(self,user:DomainUser)->DomainUser:logger.info("Attempting to create user with email: %s",user.email)django_user=DjangoUser.objects.create_user(email=user.email,first_name=user.first_name,last_name=user.last_name,)logger.info("User created successfully with ID: %s",django_user.id)returnself._to_domain_user(django_user)
Adicionamos testes de integração para validar o comportamento do tratamento global de exceções, garantindo que a API retorne respostas padronizadas para diferentes tipos de erros.
Exemplo: Teste de Login com Credenciais Inválidas (project/core/tests/integration/test_auth_api.py)¶
Este teste verifica se, ao tentar logar com credenciais inválidas, a API retorna um HTTP 400 Bad Request com a mensagem de erro esperada, conforme configurado pelo custom_exception_handler para ValueError.
# project/core/tests/integration/test_auth_api.pyfromrest_frameworkimportstatusfromrest_framework.testimportAPITestCase# ... outras importações ...classAuthAPITest(APITestCase):# ... setUp e outros testes ...deftest_login_failure_invalid_password(self):data={"email":self.email,"password":"wrongpassword"}response=self.client.post(self.login_url,data,format="json")self.assertEqual(response.status_code,status.HTTP_400_BAD_REQUEST)self.assertIn("detail",response.data)self.assertEqual(response.data["detail"],"Invalid credentials")deftest_login_failure_user_not_found(self):data={"email":"nonexistent@example.com","password":self.password}response=self.client.post(self.login_url,data,format="json")self.assertEqual(response.status_code,status.HTTP_400_BAD_REQUEST)self.assertIn("detail",response.data)self.assertEqual(response.data["detail"],"Invalid credentials")
Exemplo: Simulação de Erro Interno Inesperado (project/core/tests/integration/test_user_api.py)¶
Este teste simula um erro interno (RuntimeError) em um endpoint da API para verificar se o tratamento global de exceções o captura e retorna um HTTP 500 Internal Server Error com uma mensagem genérica e o traceback em modo DEBUG.
# project/core/tests/integration/test_user_api.pyfromrest_frameworkimportstatusfromrest_framework.testimportAPITestCasefromdjango.confimportsettings# Necessário para verificar settings.DEBUG# ... outras importações ...classUserAPITest(APITestCase):# ... setUp e outros testes ...deftest_global_exception_handler_internal_server_error(self):headers=self.get_auth_headers(self.admin_access_token)response=self.client.get(f"{self.user_list_url}?raise_error=true",**headers,format="json")self.assertEqual(response.status_code,status.HTTP_500_INTERNAL_SERVER_ERROR)self.assertIn("detail",response.data)self.assertEqual(response.data["detail"],"Ocorreu um erro inesperado.")self.assertIn("status_code",response.data)self.assertEqual(response.data["status_code"],500)ifsettings.DEBUG:self.assertIn("traceback",response.data)
Criação do arquivo de documentação: Criado docs/development/logging-error-handling.md.
Atualização do config/mkdocs.yml: Adicionado o link para o novo documento na navegação.
Configuração de Logs: Adicionada a configuração de LOGGING em project/project/settings.py.
Criação do Middleware: Criado project/core/middleware/custom_exception_middleware.py com o handler de exceções.
Integração do Middleware: Adicionado o CustomExceptionMiddleware à lista MIDDLEWARE e configurado REST_FRAMEWORK["EXCEPTION_HANDLER"] em project/project/settings.py.
Uso de Logs no Código: Inseridas chamadas de logger.info, logger.warning e logger.debug em project/core/domain/use_cases/user_use_cases.py e project/core/repositories/user_repository_impl.py.
Adição de Testes de Integração: Adicionado test_global_exception_handler_internal_server_error em project/core/tests/integration/test_user_api.py para validar o comportamento do tratamento de exceções 500.
Atualização da Documentação: Este documento foi atualizado com todos os detalhes e exemplos das etapas de implementação.
Remoção da Lógica Temporária: A lógica raise RuntimeError será removida de project/core/api/v1/views/user.py após a finalização da documentação.