Olá, caros leitores!
Há algumas semanas, eu e o Daniel Serafim, grande companheiro do blog, estávamos compartilhando algumas rotinas para capturar o máximo de informações de uma exceção ocorrida na aplicação, de forma que facilite o rastreamento e a correção do erro. Este compartilhamento resultou no artigo de hoje, no qual apresento a criação de uma rotina básica – porém, útil – para captura de exceções em uma aplicação.
Introdução
Em algumas ocasiões, temos dificuldade em reproduzir os erros que acontecem em nossas aplicações em produção, muitas vezes pela massa de dados, configuração do ambiente ou simplesmente por conta de uma sequência especÃfica de passos que provocam a exceção. Gravar um vÃdeo com a ocorrência do erro é uma alternativa, no entanto, nem sempre viável. É possÃvel que o usuário não consiga refazer os passos que causou a exceção, ou as informações contidas no próprio vÃdeo não sejam suficientes para identificar o problema.
Em vista desse contexto, elaborar uma rotina para captura de exceções é uma solução bem coerente. De modo geral, a rotina atua como um listener, monitorando as exceções que ocorrem no sistema e gravando-as em um arquivo de texto ou em uma tabela do banco de dados para posterior análise da equipe de desenvolvimento.
Atualmente, com o Delphi, o desenvolvedor pode optar pela instalação de excelentes ferramentas para essa finalidade, como o EurekaLog ou madExcept, que fornecem uma série de recursos para captura de exceções. Porém, alguns desenvolvedores preferem criar suas próprias rotinas para reduzir ou evitar a instalação de componentes de terceiros. Este artigo é para estes desenvolvedores!
Rotina de captura de exceções
Codificaremos, a seguir, uma rotina simples que armazenará as seguintes informações ao ocorrer uma exceção:
- Data e hora
- Mensagem da exceção
- Classe da exceção
- Nome do formulário aberto
- Nome da Unit
- Nome do controle visual focado
- Nome do usuário
- Versão do Windows
- Imagem do formulário
Com todas essas informações, o rastreamento torna-se bem mais fácil, não? 🙂
Obtendo o nome do usuário
Acho importante iniciarmos pelos métodos que retornam as três últimas informações, já que exigem uma codificação extra. O primeiro deles, que retorna o nome do usuário, é bem simples:
1 2 3 4 5 6 7 8 9 10 |
function TForm1.ObterNomeUsuario: string; var Size: DWord; begin // retorna o login do usuário do sistema operacional Size := 1024; SetLength(result, Size); GetUserName(PChar(result), Size); SetLength(result, Size - 1); end; |
Obtendo a versão do Windows
Para obter a versão do Windows, podemos trabalhar com a classe TOSVersionInfo
nativa do Delphi, analisando os resultados do número de build do sistema operacional:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function TForm1.ObterVersaoWindows: string; begin case System.SysUtils.Win32MajorVersion of 5: case System.SysUtils.Win32MinorVersion of 1: result := 'Windows XP'; end; 6: case System.SysUtils.Win32MinorVersion of 0: result := 'Windows Vista'; 1: result := 'Windows 7'; 2: result := 'Windows 8'; 3: result := 'Windows 8.1'; end; 10: case System.SysUtils.Win32MinorVersion of 0: result := 'Windows 10'; end; end; end; |
Obtendo a imagem do formulário
A captura da imagem do formulário, por sua vez, também é uma rotina pequena, responsável por associar a imagem dentro de um objeto do tipo TBitmap
. No entanto, para que o tamanho do arquivo da imagem fique menor, recomendo a gravação do arquivo em formato JPEG utilizando um objeto da classe TJpegImage
, conforme abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
uses Vcl.Imaging.jpeg; procedure TForm1.GravarImagemFormulario(const NomeArquivo: string); var Bitmap: TBitmap; JPEG: TJpegImage; begin JPEG := TJpegImage.Create; try Bitmap := Formulario.GetFormImage; JPEG.Assign(Bitmap); JPEG.SaveToFile(Format('%s\%s.jpg', [GetCurrentDir, NomeArquivo])); finally JPEG.Free; Bitmap.Free; end; end; |
Com as três funções acima codificadas, partiremos para a parte principal. Adicione um componente TApplicationEvents
, da paleta Additional, no formulário principal do sistema.
Por fim, codificaremos o evento OnException
do componente para “interceptar” a exceção e obter os dados da aplicação naquele momento.
Observe que iremos declarar uma variável chamada DataHora
. Usaremos essa variável como nome dos arquivos das imagens, de forma que o desenvolvedor possa descobrir de qual exceção cada imagem está relacionada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
var CaminhoArquivoLog: string; ArquivoLog: TextFile; DataHora: string; begin // Obtém o caminho do arquivo de log CaminhoArquivoLog := GetCurrentDir + '\LogExcecoes.txt'; // Associa o arquivo à variável "ArquivoLog" AssignFile(ArquivoLog, CaminhoArquivoLog); // Se o arquivo existir, abre para edição, // Caso contrário, cria o arquivo if FileExists(CaminhoArquivoLog) then Append(ArquivoLog) else ReWrite(ArquivoLog); // Obtém a data e hora atual para usar como "identificador" da imagem DataHora := FormatDateTime('dd-mm-yyyy_hh-nn-ss', Now); // Chama o método de captura da imagem do formulário GravarImagemFormulario(DataHora); // Escreve os dados no arquivo de log WriteLn(ArquivoLog, 'Data/Hora.......: ' + DateTimeToStr(Now)); WriteLn(ArquivoLog, 'Mensagem........: ' + E.Message); WriteLn(ArquivoLog, 'Classe Exceção..: ' + E.ClassName); WriteLn(ArquivoLog, 'Formulário......: ' + Screen.ActiveForm.Name); WriteLn(ArquivoLog, 'Unit............: ' + Sender.UnitName); WriteLn(ArquivoLog, 'Controle Visual.: ' + Screen.ActiveControl.Name); WriteLn(ArquivoLog, 'Usuário.........: ' + ObterNomeUsuario); WriteLn(ArquivoLog, 'Versão Windows..: ' + ObterVersaoWindows); WriteLn(ArquivoLog, StringOfChar('-', 70)); // Fecha o arquivo CloseFile(ArquivoLog); end; |
É isso aÃ! A nossa rotina de captura de exceções está pronta!
Para testá-la, adicione um botão na tela e provoque uma exceção, como uma conversão incorreta:
1 |
StrToInt('A'); |
Em seguida, navegue até a pasta da aplicação e verifique que dois arquivos foram criados: “LogExcecao.txt” e o arquivo de imagem. Para cada exceção subsequente, a aplicação criará uma nova imagem e atualizará o arquivo de log com os dados da nova exceção. Legal, hein?
Uma observação: para que os testes sejam mais efetivos, até mesmo em função da captura correta da imagem da tela, execute a aplicação fora do Delphi, como se estivesse em produção.
Bom, embora toda essa codificação já seja o suficiente, trago ainda algumas sugestões que possam ser úteis:
1) Mostre uma mensagem após gravar a exceção em log
Você deve ter notado que, ao ocorrer uma exceção, os arquivos são criados, mas nenhuma mensagem é exibida ao usuário, já que “interceptamos” a exceção. Esse comportamento, claro, não é o ideal, já que o usuário pode julgar que a aplicação está funcionando normalmente. Portanto, no final do evento OnException
, recomendo adicionar uma mensagem para alertar o usuário de que houve um evento inesperado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var StringBuilder: TStringBuilder; begin {...} StringBuilder := TStringBuilder.Create; try // Exibe a mensagem para o usuário StringBuilder .AppendLine('Ocorreu um erro na aplicação.') .AppendLine('O problema será analisado pelos desenvolvedores.') .AppendLine(EmptyStr) .AppendLine('Descrição técnica:') .AppendLine(E.Message); MessageDlg(StringBuilder.ToString, mtWarning, [mbOK], 0); finally StringBuilder.Free; end; end; |
2) Envie um e-mail periodicamente contendo o arquivo de log e as imagens
Eu adicionei essa opção em um dos meus projetos. Dado um perÃodo, que pode ser diário, semanal, mensal ou sob demanda, a aplicação envia um e-mail para o meu endereço com o log e as imagens compactadas em um único arquivo. Com isso em mãos, consigo identificar os erros e aplicar as correções com mais agilidade, à s vezes de forma até antecipada.
Para codificar essa funcionalidade, pode-se criar uma rotina de envio de e-mails com o TIdSMTP e dispará-la em uma Thread paralela com o TTask
para não “travar” o usuário.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
uses System.Threading; var EnvioEmail: ITask; begin EnvioEmail := TTask.Create( procedure begin EnviarEmailComLogs; end); EnvioEmail.Start; end; |
3) Grave as exceções em uma tabela do banco de dados
Uma boa alternativa é gravar as exceções em uma tabela do banco de dados ao invés de um arquivo de log. Neste caso, o desenvolvedor poderá desenhar uma tela para visualização das exceções em uma Grid, semelhante ao que o Daniel Serafim desenvolveu em seu projeto (boa, Daniel!):
4) Crie uma classe para atuar como wrapper da exceção
Os códigos desse artigo são apenas demonstrações. Caso você realmente considere agregá-los à sua aplicação, crie uma classe especÃfica para escrever os dados no arquivo de log, capturar as imagens e, opcionalmente, enviar e-mails. Lembre-se do princÃpio da responsabilidade única!
Surgiu alguma dúvida nos códigos? Baixe o projeto de exemplo deste artigo desenvolvido em Delphi Tokyo no link abaixo e estude as codificações:
Download do projeto de exemplo de captura de exceções
Grande abraço, pessoal!
Fantástico André. Em uma empresa a qual trabalhei, também gravávamos o log da query nas exceções, para isso, no nosso “on E: Exception” chamávamos uma função a qual pegava o parâmetro que era o SQL gerado e logava isso em arquivo, além de conter o nome da tela que originou o erro e o próprio Exception. Ajudava muito na detecção de erros durante a homologação e eventualmente em produção.
Fala, Marcão!
Gostei dessa abordagem do armazenamento das queries em arquivos de log! Se não me engano, o FireDAC já traz um mecanismo para fazer esse tipo de monitoramento. Vou estudar mais sobre isso e elaborar um artigo. 🙂
Obrigado! Abração!
Parabéns Celestino, é pequenas coisas que fazem diferença no produto final.
Uma coisa simples que ajuda muito na hora em que a aplicação está em produção, economizando alguns cabelos.
Forte Abraço
Obrigado, Victor! Essa rotina, embora simples, fornece uma boa ajuda no rastreamento de erros.
Agradeço pela sua colaboração na correção da rotina que retorna a versão do Windows! 🙂
Grande abraço!
Fala André, muito obrigado.
Dica valiosa e importante. O armazenamento das exceções no banco é interessante pois você consegue visualizar todas as exceções e de todos os usuários em único lugar.
Abraço.
Bem obervado, Daniel!
Em ambientes multi-usuário, talvez a melhor opção realmente seja o armazenamento das capturas no banco de dados. Por estarem centralizadas, o desenvolvedor não precisa se preocupar em recolher o arquivo de log de cada estação de trabalho.
Grande abraço!
Boa tarde!
O método de captura pode ser feito somente no “form” em que ocorreu a exceção?
Abraço
Olá, Elton, tudo bem?
Se você pretende capturar as exceções apenas de um formulário especÃfico, recomendo utilizar as estruturas tradicionais do try..except. O componente TApplicationEvents monitora as exceções de todos os formulários da aplicação, então não atenderia a sua necessidade.
Abraço!
Boa tarde André,
Existe a possibilidade de compartilhar a função “EnviarEmailComLogs”, pois como você mesmo descreveu ela já envia os logs e imagens compactadas seria interresante.
Olá, Adailson, tudo jóia?
O envio de e-mail pode ser baseado em um artigo aqui mesmo do blog:
https://www.andrecelestino.com/delphi-xe-envio-de-e-mail-com-componentes-indy/
Veja a parte do código que se refere aos anexos. É bem ali!
Abraço!
Muinto bom como já é de esperar de você, André. Estava trabalhando em exceções gravando no banco de dados e adicionei as imagens, então quando seleciono uma exceções ao lado, mostra a imagem. Valeu mesmo.
Legal, Daniel!
As imagens realmente ajudam muito no rastreamento de erros, já que nos permite analisar o status dos controles do formulário e a visualização de dados no momento em que a exceção ocorreu.
Abraço!
Muito bom, André. Testei o código fonte com o ReportMemoryLeak ativo.
Verifiquei que ao gerar o JPEG do form o delphi registra um memory leak.
Procurei na web e não achei a solução. Parece ser um problema do Delphi.
Você pode confirmar isso?
Boa noite, Josimar!
Muito bem observado! Fiz o mesmo teste aqui e também recebi o Memory Leak.
Após algumas pesquisas, notei que o código do artigo está incorreto. Não é necessário criar uma instância de TBitmap. A própria função GetFormImage já faz este trabalho, portanto, segue a correção:
Já alterei o artigo também.
Muito obrigado, Josimar! Abração!
Top. Parabens pelas dicas.
Opa, obrigado, Rodrigo!
Bom dia, André! Parabéns pelo artigo, ele é muito útil, mas eu tenho uma dúvida.
A “procedure” “OnException” do “ApplicationEvents” só será chamada caso não haja nenhuma tratamento de try/except no local onde ocorreu a exceção. Há alguma forma para que, diante qualquer exceção, a procedure “OnApplicationEventsException” seja chamada?
Questiono isso pois queria adicionar à OO do software um tratamento para todas as exceções geradas pelo mesmo, sem a necessidade de alterar formulário por formulário.
Boa noite, Lucas. Ótima questão!
O evento
OnException
do componenteTApplicationEvents
só é chamado para exceções não tratadas no código. Se houver um try/except no método em que ocorreu da exceção, significa que a exceção já está sendo tratada, portanto, o eventoOnException
é ignorado.Nessas circunstâncias, para que o evento
OnException
seja chamado, é necessário adicionar um raise no except local:Abraço!
Boa tarde, André ótima dica.
Porém, não consigo gravar no banco de dados.
Consegui fazer toda rotina funcionar, porém quando tento Associar uma TFDQuery a uma TFDConnection ( que já estava em uso), tenho Access violation.
Olá, Welbert, boa noite.
Você pretende gravar as exceções no banco de dados? Se este for o caso, é necessário criar uma tabela e preenchê-la com os dados da exceção no evento
OnException
do componenteTApplicationEvents
.Abraço!
André, li todo o post e vai me ajudar muito.
Só tenho uma duvida pois minha aplicação é multithread onde eu tenho alem das threads principal da vcl um outra thread que faz o controle de execução de processos(outras threads) estou implementando essa rotina nessa minha thread de controle de execução, gostaria de saber se existe uma forma de pegar qual foi a procedure que gerou a exceção na outra thread que é de execução.
Olá, Cristiano, desculpe-me pela demora.
Vou entrar em contato com você para entender melhor o cenário.
Abraço!
Parabéns André, porém tem como capturar a Imagem do formulário em Firemonkey sem ser VCL?
Olá, João! Pergunta interessante!
No Firemonkey, você pode usar o método MakeScreenshot do componente TLayout. Veja um exemplo:
Abraço!
Parabéns André. Esse Post é a “ferramenta” da ferramenta.
Tive um problema estranho, ao debugar não localizou System.SysUtils. Pode ser em virtude da minha versão do Delphi (6). No caso da criação e salvamento do JPGE fiz trêz adaptações importantes para meu gost , espero que seja útil aos Navegantes.
Personalizei a pasta de Logs e comprimi os JPGs, já que não necessito tanto de qualidade porém cada Byte ganho (ou evitado rsss) é uma conquista. E achei melhor capturar a tela, pois a função estava sempre capturando sempre o FormPrincipal.
Saudações a todos
Perfeito, Rogério!
Agradeço pela sua colaboração em compartilhar o seu código. Com certeza poderá ajudar outros desenvolvedores!
No Delphi 6 realmente não existe “System.SysUtils”, apenas “SysUtils”. O conceito de namespaces foi adicionado a partir da famÃlia XE do Delphi.
Abraço!
Isso sim é uma ferramenta simples e efetiva para minimizar problemas. Parabéns, e muito obrigado.
Legal, Fernando! Muito obrigado pelo feedback!