[Delphi] Rotina básica para captura de exceções

[Delphi] Rotina básica para captura de exceções

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.

 

Em algumas ocasiões, temos dificuldade em reproduzir os erros que acontecem em produção em nossas aplicações, 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!
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? 🙂

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:

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;

 

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:

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;

 

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:

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.

Componente TApplicationEvents

Por fim, codificaremos o evento OnException do componente para “interceptar” a exceção e obter os dados da aplicação naquele momento.
Observe que vamos 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.

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:

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?
Apenas 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:

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.

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!):

Exemplo de visualização de exceções registradas no banco de dados

 

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!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

14 comentários

  1. 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.

    1. 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!

  2. 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

    1. 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!

  3. 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.

    1. 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!

  4. Boa tarde!
    O método de captura pode ser feito somente no “form” em que ocorreu a exceção?
    Abraço

    1. 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!

  5. 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.

  6. 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.

    1. 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!

  7. 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?

    1. 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:

      var
        Bitmap: TBitmap;
        JPEG: TJpegImage;
      begin
        JPEG := TJpegImage.Create;
        try
          Bitmap := Self.GetFormImage;
          JPEG.Assign(Bitmap);
          JPEG.SaveToFile('D:\arquivo.jpg');
        finally
          JPEG.Free;
          Bitmap.Free;
        end;
      end;

      Já alterei o artigo também.
      Muito obrigado, Josimar! Abração!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.