Artigo Revista MundoDotNet - Tratamento de Erros
Origem: Kerne.org, a enciclopédia livre.
Registro e Tratamento de Erros
José Roberto Kerne (mono@kerne.org): Formado em Processamento de Dados em 2000 pela Sociesc, atua na área de desenvolvimento de software desde 1996. Desenvolvedor em PHP, .Net/C# e Java é Diretor de Projetos da empresa Devel Sistemas de Informática e atua com .Net/C# como coordenador do projeto GeNova, servidor MMORPG Ultima Online, projeto mantido e apoiado pelo MonoBrasil.
Neste artigo você encontrará conceitos, idéias e exemplos de implementação de um sistema de registro e tratamento de erros que pode ser facilmente adaptado e implementado em seu software escrito em C# de acordo com suas necessidades.
Introdução
É comum, talvez até habitual para nós, desenvolvedores, iniciarmos o planejamento de um projeto partindo da organização em camadas, módulos, definições de quais ferramentas e linguagem de programação será utilizada, qual banco de dados, modelo de documentação, design visual e por fim, os tão esperados testes da aplicação. Embora pareça pouco necessário, um bom sistema de registro e tratamento de erros é importante desde o início da fase de desenvolvimento, mas infelizmente este é um dos recursos mais "deixados de lado" em grande parte dos projetos.
Este artigo mostra alguns pontos importantes no processo de codificação utilizando uma aplicação simples de registro e tratamento de erros, que auxiliará o desenvolvedor em testes de sistema, sem a necessidade ou mesmo dependência de uma ferramenta de IDE específica para esta atividade. Com estruturas simples e de fácil implementação, é apresentada uma forma prática e simples de tratar erros que possivelmente gerariam a perda ou falha de processos ou de dados.
Como, Onde e Por quê?
Muitos dos aspectos envolvendo tratamento de erros baseiam-se em falhas de sistema. Uma falha comum de sistema – uma Exception - pode ocorrer a partir de um erro de tratamento de uma variável, erros de I/O (entrada e saída de dados), conexão com servidores e/ou bancos de dados etc. Estas falhas podem em sua maioria causar perdas de dados e interrupções de continuidades de processo irrecuperáveis. Para tratar blocos de código com possibilidades de erros, usamos o conjunto try, catch e finally que que podem afetar diretamente o comportamento do software durante e após a ocorrência de erros.
Um exemplo simples de falha de processo é uma transação onde um cadastro inativo de um cliente gera o fechamento de contas para posterior cobrança.
Vejamos o escopo a seguir:
Escopo 1 - Inativação de cadastro de cliente
Atividade: Inativar Cliente
Etapa 1: Alterar status do Cliente para Inativo
Etapa 2: Realizar fechamento de Contas, totalizando valores
Etapa 3: Incluir registro em cobranças em aberto gerando novo faturamento (Contas a Receber)
Análise 1: Caso um erro ocorra na etapa 1 nenhuma informação é perdida e o Cliente continua com status Ativo. Neste caso poderíamos efetuar o registro do erro na aplicação de tratamento de erros e apenas retornar uma mensagem ao usuário informando que houve uma falha ne atualização do Cliente. Informar, por exemplo, que o erro foi de conexão com o banco de dados não ajudaria muito a vida do usuário final, mas ter o registro de tal informação é excencial para os responáveis pela manutenção do sistema.
Listagem 1. Trecho de código referente ao Escopo 1
public class Cadastro : BaseSQL
{
public static int codigo = 0;
public static string status;
public void CadastroCliente(string Acao) {
if (Acao.Equals("Inativar")) {
Inativar()
}
}
// Ronatina de Inativação de Clientes
public static void Inativar() {
status = "I";
// Procedimento de atualização de banco de dados
try {
Salva();
}
catch ( Exception e ){
// Identificação do módulo que gerou o erro
OdbcLog.Modulo = "Cadastro de Clientes";
// Etapa onde ocorreu o erro
OdbcLog.Acao = "Inativar";
// Variável log contem a Exception
OdbcLog.Log = e.Message;
// Método de gravação de erro
OdbcLog.Grava();
}
finally {
// Para finalizar, mostrar mensagem de erro ao usuario
Console.WriteLine("cadastro indisponível no momento!");
}
}
}
Análise 2: Caso o processo apresente erros a partir das etapas 1 ou 2 os dados alterados devem ser retornados ao estado anterior, evitando assim inconsistência de dados. Ao reestabelecer o estado original, finalmente mostrar mensagem de erro em tela ao usuário final, registrando detalhes sobre o erro para posterior análise do administrador de sistemas.
Existem diferenças não somente na implementação das etapas mas também na aplicação de conceitos do escopo inicial do projeto e tratamento dos erros. Não existe sistema capaz de prever todo e qualquer erro possível, este é o trabalho do desenvolvedor.
Para o registro dos erros, cria-se uma tabela que será utilizada para registro dos erros da aplicação, conforme a listagem 2.
Listagem 2. Criação de Base de Dados MySQL
create table log (
id bigint auto_increment primary key,
modulo varchar(40),
submodulo varchar(80),
acao varchar(80),
atributos text,
log text,
data datetime
);
Para manipular a tabela acima, utilizamos a seguinte classe mostrada na listagem 3.
Listagem 3. Implementação de Classe OdbcLog.cs
using System;
using System.Data;
using ByteFX.Data.MySqlClient;
/* Classe OdbcLog implementa Classe Database */
public class OdbcLog : Database {
public static void Init(string[] argv) {
Console.WriteLine("OdbcLog-0.1 iniciado!");
if ( argv.Length > 0 ) {
// Checar possibilidades
try {
// Executar Auto-Teste
if ( Array.BinarySearch(argv,"-t")>=0 ) Test();
// Ativar Debug
if ( Array.BinarySearch(argv,"-d")>=0 ) debug=true;
}
catch ( Exception e ) {
log = e.Message;
Conecta();
Salva();
}
}
}
/* Método para testes unitários da classe*/
public static void Test() {
Console.WriteLine("Iniciando Auto-Teste");
Console.WriteLine("--------------------");
Conecta();
/* Carregando variáveis de log para registro */
modulo = "Financeiro"; // Nome do módulo do software
submodulo = "Cadastro de Clientes"; // Nome do submodulo (opcional)
acao = "Inclusao"; // Nome da ação no sistema
atributos = "var1=2,var2=1,var3=5"; // Atributos/variáveis em uso
log = "texto do erro que ocorreu na tela"; // Texto ou Exception
Console.WriteLine(
"modulo = " + modulo + "\n" +
"submodulo = " + submodulo + "\n" +
"acao = " + acao + "\n" +
"atributos = " + atributos + "\n" +
"log = " + log );
Console.WriteLine("--------------------");
/* Envia comando de salvamento de dados para Classe OdbcLog */
Salva();
}
}
Considerações sobre Listagem 3 - OdbcLog.cs
A classe OdbcLog.cs, por se tratar de uma classe que pode ser utilizada para testes de laboratório (compilação standalone ou biblioteca) ou como parte integrante de sua árvore de códigos fonte, a primeira observação importante é sobre o método Init(). Este método pode ser renomeado para Main() para a classe ser compilável de forma independente, o que torna seu uso possível como biblioteca externa. Mantendo-se o Init() apenas isolamos a auto-execução de um registro de dados durante a instância da classe.
Outro método interessante e que trata o início do tratamento de erros é IsCritical() (mostrado na listagem 4), que retorna para a classe principal os valores true ou false, dependendo do tipo Exception encontrado na aplicação. Este método pode ser utilizado em qualquer sistema, sendo muito útil para testar, por exemplo, se deve haver continuidade na execução. Em um erro de falta de memória, o retorno true pode indicar para o sistema que ocorreu um problema crítico e a continuidade de execução está comprometida.
/* Método de tratamento básico de erros críticos */
public static bool IsCritical(Exception ex)
{
if (ex is OutOfMemoryException) return true;
if (ex is AppDomainUnloadedException) return true;
if (ex is BadImageFormatException) return true;
if (ex is CannotUnloadAppDomainException) return true;
if (ex is ExecutionEngineException) return true;
if (ex is InvalidProgramException) return true;
if (ex is System.Threading.ThreadAbortException)
return true;
return false;
}
Listagem 4: Implementação do método IsCritical
Para maiores detalhes, recomendo um breve salto para a Listagem 11 no trecho de código "retorno = OdbcLog.IsCritical( e )". Observe que no bloco try lé forçado um erro de tempo de execução, gerado pelo teste em if. A variável retorno é checada no bloco finally apenas para ilustrar uma das formas de teste e uso da classe OdbcLog.cs, mostrando uma mensagem de OK caso o erro encontrado não seja crítico, evitando que a aplicação finalize sua execução, porém registrando a mensagem "Array index is out of range." com a data e hora da ocorrencia e outras informações adicionais, permitindo ao administrador de sistemas verificar qual o problema em maiores detalhes.
Utilizando OdbcLog.cs como biblioteca
Após renomear o método Init() para Main(), basta compilar a classe e executá-la com o comando descrito na listagem 5 a seguir:
Listagem 5. Executando a classe de forma independente
$ mono OdbcLog.exe -t -d
Executando a classe independentemente
Ao utilizar os parâmetros "-t" ou "-d" em linha de comandos, passamos ao método principal dois parâmetros carregados pela string argv. Esta string é testada no método Main() identificando "-t" para exeução de auto-teste e "-d" para execução em modo debug. Utilize a opção "-t" para executar o teste após a implementação do código das listagens 3 e 4 para garantir que a classe está funcionando corretamente.
Classe OdbcLog.cs - Segunda parte
A listagem 6 mostra a segunda parte do código da classe OdbcLog.cs, contendo a classe independente de manipulação de banco de dados chamada aqui convenientemente de Database. Note que neste método todas as variáveis são do tipo static por se tratarem de variáveis que serão acessadas por classes independentes. Como primeiro passo configure as variáveis de conexão com o banco de dados de acordo com as permissões configuradas no serviço MySQL em seu servidor.
Sobre o MySQL
Possíveis erros como falha de conexão, erro de instrução SQL e outros serão direcionados para a saida padrão, neste caso a console. Observe que no método Conecta() a variável metodo contem um código de identificação do erro gerado. Note também que o uso de uma variável de controle como a variável erro pode ser necessária em alguns casos, facilitando principalmente o teste através de classes independentes. O funcionamento é semelhante ao método IsCritical() descrito na listagem 4, porém sua checagem é posterior a execução da sequência de instruções, além de ser opcional.
A utilização de variáveis estáticas em uma classe independente é simples, porém sempre recomenda-se cautela no uso de variáveis estáticas. Como esta classe não terá processamento simultâneo, ou seja, não há concorrência na utilização das variáveis estáticas da classe e sim um carga única de sequência, como listado abaixo, torna o uso desta classe torna-se extremamente simples;
1) Carregamento do método
2) Carga de valores para variáveis
3) Conexão com banco de dados
4) Gravação dos dados
5) Desconexão do banco de dados
Listagem 6. Implementação de Classe OdbcLog.cs - continuação
public class Database {
/* Variaveis de controle interno */
public static string metodo = "";
public static bool erro = false;
public static bool debug = false;
/* Conexao com banco de dados MySQL */
public static IDbConnection DBConnection;
public static IDbCommand DBCommand;
public static string DatabaseServer = "localhost";
public static string DatabaseName = "OdbcLog";
public static string DatabaseUserID = "root";
public static string DatabasePassword = "";
public static string connectionString =
"Server=" + DatabaseServer +
";Database=" + DatabaseName +
";User ID=" + DatabaseUserID +
";Password=" + DatabasePassword + ";";
/* Implementação de Getters e Setters necessários */
public static int id_;
public static int id {
get { return ( id_ ); }
set { id_ = value; }
}
public static string modulo_;
public static string modulo {
get { return ( modulo_ ); }
set { modulo_ = value; }
}
public static string submodulo_;
public static string submodulo {
get { return ( submodulo_ ); }
set { submodulo_ = value; }
}
public static string acao_;
public static string acao {
get { return ( acao_ ); }
set { acao_ = value; }
}
public static string atributos_;
public static string atributos {
get { return ( atributos_ ); }
set { atributos_ = value; }
}
public static string log_;
public static string log {
get { return ( log_ ); }
set { log_ = value; }
}
/**
* Metodo de conexao ao banco de dados
*/
public static void Conecta() {
/**
* Escopo de implementação
* 001) Conexão com banco de dados
* 002) Gravação de dados na tabela
* 003) Desconexão do banco de dados
*/
if( debug ) Console.WriteLine("Conectando ao banco de dados");
try {
DBConnection = new MySqlConnection( connectionString );
DBConnection.Open();
erro = false;
if( debug ) Console.WriteLine("Conectado!");
}
catch ( Exception ex ) {
metodo = "DB001 - Conecta() " + ex.Message;
Console.WriteLine("OdbcLog: Erro em sistema de log - " + metodo);
erro = true;
}
}
A string "DB001" na variável metodo, identifica facilmente que trata-se de uma ação de banco de dados, instrução de sequência 1 (um). Este número de sequência é utilizado seguindo a mesma numeração do "escopo de implementação da classe", que é parte integrante da documentação do software. Observe que o código "001" corresponde exatamente a instrução de conexão com banco de dados.
O trecho da listagem 7 mostra o método de gravação de dados:
public static void Salva() {
try {
if ( erro == false ) {
/* Instanciar comando SQL para exeução – Preparar SQL */
DBCommand = DBConnection.CreateCommand();
/* Carregar variável de consulta SQL, preenchendo valores a
serem registrados */
DBCommand.CommandText = "INSERT INTO log VALUES (0," +
"'" + modulo + "'" +
", '" + submodulo + "'" +
", '" + acao + "'" +
", '" + atributos + "'" +
", '" + log + "'" +
", now() )";
if( debug ) Console.WriteLine("Salvando dados...");
DBCommand.ExecuteNonQuery();
}
}
catch ( Exception ex ) {
metodo = "DB002 – Salva() " + ex.Message;
Console.WriteLine("OdbcLog: Erro em sistema de log - " + metodo);
}
}
/* Método de gravação de registro de log */
public static void Grava() {
Conecta();
Salva();
Desconecta();
}
Listagem 7: gravação de dados
Você deve ter observado o método Salva() e o método Grava(). Apesar de parecerem redundantes há uma observação importante aqui. Acima citamos o uso da sequência de execução da classe, onde a última instrução era a desconexão logo após a gravação de dados, e é exatamente aí que entra o método Grava(). Caso haja uma sequencia de testes com possível geração de erros, pode-se manter a mesma conexão ativa com o banco de dados, executando o método Salva() e ao final da sequência executar o método Desconecta() que é mostrado na listagem 8.
/* Método para desconexão do banco de dados. Carregado automaticamente pelo
método Salva() */
public static void Desconecta() {
if( debug ) Console.WriteLine("Desconectando...");
try {
DBConnection.Dispose();
DBConnection = null;
}
catch ( Exception ex ) {
/* Em caso de erro durante desconexão, enviar mensagem de erros
direatamente para console, evitando Exception */
metodo = "DB003 – Desconecta() " + ex.Message;
Console.WriteLine("OdbcLog: Erro em sistema de log - " + metodo);
}
}
}
Listagem 8: desconectando do banco de dados
Finalizando a classe OdbcLog.cs temos desconexão com o banco de dados, seguido do bloco de tratamento de erro try/catch. Este faz uso novamente da variável metodo que informa a sequência "003" do escopo de implementação. Vale lembrar que esta implementação é apenas ilustrativa e deve ser encarada como uma sugestão e não como regra.
Compilando
O leitor poderá optar em transformar este objeto em biblioteca, e por fim adicioná-la ao seu ambiente de compilação ou incluir este código em sua árvore de código fonte. Na listagem 9 (para Linux) e listagem 10 (Windows) temos o exemplo de como utilizar este código como biblioteca:
Listagem 9: Compilando no Linux
$ mcs -define:MONO -debug -r:System.Data.dll -r:ByteFX.Data.dll -out:OdbcLog.dll OdbcLog.cs '''Listagem 10:''' compilando no Windows <pre> C:\> mcs.exe -define:MONO -debug -r:System.Data.dll -r:ByteFX.Data.dll -out:OdbcLog.dll OdbcLog.cs
Exemplo de implementação
Em seguida, na listagem 11, implementamos a classe Teste, que fará uso do OdbcLog.cs como biblioteca.
Listagem 11. Implementação de classe Teste.cs
using System;
public class Teste {
public static bool retorno = true;
public static void Main( string[] argv ) {
Console.WriteLine("Programa de teste iniciado!");
try {
/* Teste [if] passível de falha em tempo de exeução */
if ( argv[1] == "teste" ) ; // Implementação...
}
catch ( Exception e ) {
/* Registrar Log */
OdbcLog.modulo = "Programa de Testes";
OdbcLog.acao = "teste";
OdbcLog.log = e.Message;
OdbcLog.Grava();
/* Tratamento de erro crítico */
retorno = OdbcLog.IsCritical( e );
}
finally {
if ( retorno == true ) Console.WriteLine("Programa OK");
else Console.WriteLine("Erro crítico");
}
}
}
- Considerações
- No trecho de código if ( argv[1] == "teste") notamos algo muito importante: não é possível garantir que sempre haverá o elemento 1 na string argv.
Compilação para Teste
O exemplo mostrado na listagem 12 (Linux) e listagem 13 (Windows) mostra como compilar um programa independente, utilizando o exemplo OdbcLog deste artigo como referência de biblioteca externa. Se preferir, poderá incluir o OdbcLog a sua árvore de arquivos, embutindo-o em seu software.
Listagem 12: Compilando no Linux
$ mcs -define:MONO -debug -r:System.Data.dll -r:ByteFX.Data.dll -out:Teste.exe Teste.cs OdbcLog.cs '''Listagem 13:''' Compilando no Windows <pre> C:\> mcs.exe -define:MONO -debug -r:System.Data.dll -r:ByteFX.Data.dll -out:Teste.exe Teste.cs OdbcLog.cs
Teste e Comentários
Com o programa "Teste.exe" pronto e com um possível erro, quando não houverem parâmetros passados pela console de comandos, teremos um Registro de log de erro.
Após executar o programa Teste.exe, não temos apresentação de erros em tela, mas sim um registro de erro em nosso sistema de log, conforme a listagem 14.
Listagem 14: log gerado pela aplicação
Módulo: Programa de Testes Ação: teste Log: Array index is out of range. Data: (data de execução)
Na próxima execução do programa Teste.exe, note que o parâmetro "'1" correspondente ao elemento 0 (zero) da string argv, enquanto o parâmetro "2" corresponder ao elemento 1 (um) da string argv. O programa Teste.exe espera pelo elemento 1 na string, desta forma será necessário passar dois parâmetros ao programa, de acordo com a listagem 15 (para o Linux) e listagem 16 (no Windows).
Listagem 15: Executando o programa no Linux
$ mono Teste.exe 1 2
Listagem 16: executando o programa no Windows
C:\> mono.exe Teste.exe 1 2
Possibilidades e Extensões
Uma das possibilidades ainda em registros de erros é a internacionalização das mensagens. Cada registro é realizado com dados recebidos, sendo assim, se seu software já possuir suporte a mais idiomas, basta repassar as mensagens já traduzidas ao sistema de log. Outra possibilidade abordada no programa OdbcLog.cs é a padronização de códigos de erro contendo prefixos, sufixos, números de sequência e outras técnica já largamente utilizadas em desenvolvimento de software. O retorno de valores a partir do sistema de registro de erros torna-se opcional, já que uma outra camada pode ser implementada, como o exemplo do método IsCritical implementado em OdbcLog.cs.
Considerações Finais
A utilização de uma classe de tratamento de erros é útil ao desenvolvedor como parte importante do processo de desenvolvimento do sistema, porém, sem a aplicação de um sistema de registro detalhado de ocorrências de erros e/ou falhas de processos, a tarefa de “debugging” e testes do sistema torna-se muito mais difícil. Grandes sistemas com grande consumo de recursos de rede, processamento, memória e armazenamento tornam a tarefa de identificação e correção de erros cada vez mais complexas. O registro dessas informações com o sistema em produção, possibilita um ambiente onde o desenvolvedor e o administrador de sistemas possam projetar e executar melhorias ou correções muito mais eficientes e rápidas para a maior parte dos problemas, simples ou complexos.
Referências
- MonoBrasil: http://monobrasil.sl.org.br
- MSDN: http://msdn2.microsoft.com/en-us/library/ms173162.aspx
- C# Help: http://www.csharphelp.com/archives/archive167.html
- CodeProject: http://www.codeproject.com/csharp/csmverrorhandling.asp
Para texto do artigo vide Revista Mundo Dot Net - Edição Junho/2007
ODBCLog.cs
/**
* Crie a base de dados para esta aplicação conforme SQL a seguir:
create table log (
id bigint auto_increment primary key,
modulo varchar(40),
submodulo varchar(80),
acao varchar(80),
atributos text,
log text,
data datetime
);
*/
using System;
using System.Data;
using ByteFX.Data.MySqlClient;
public class ODBCLog : Database {
public static void Init(string[] argv) {
if( debug ) Console.WriteLine("DBLog-0.1 inicializado!");
if ( argv.Length > 0 ) {
// Checar possibilidades
try {
// Executar Auto-Teste
if ( Array.BinarySearch(argv,"-t")>=0 ) Test();
// Ativar Debug
if ( Array.BinarySearch(argv,"-t")>=0 ) debug=true;
}
catch ( Exception e ) {
log = e.Message;
Conecta();
Salva();
}
}
}
public static void Test() {
Console.WriteLine("Iniciando Auto-Teste");
Console.WriteLine("--------------------");
Conecta();
modulo = "Financeiro";
submodulo = "Cadastro de Clientes";
acao = "Inclusao";
atributos = "var1=2,var2=1,var3=5";
log = "texto do erro que ocorreu na tela";
Console.WriteLine(
"modulo = " + modulo + "\n" +
"submodulo = " + submodulo + "\n" +
"acao = " + acao + "\n" +
"atributos = " + atributos + "\n" +
"log = " + log );
Console.WriteLine("--------------------");
Salva();
}
}
public class Database {
/* Variaveis de controle interno */
public static string metodo = "";
public static bool erro = false;
public static bool debug = false;
/* Conexao com banco de dados MySQL */
public static IDbConnection DBConnection;
public static IDbCommand DBCommand;
public static string DatabaseServer = "localhost";
public static string DatabaseName = "odbclog";
public static string DatabaseUserID = "usuario";
public static string DatabasePassword = "senha";
public static string connectionString =
"Server=" + DatabaseServer +
";Database=" + DatabaseName +
";User ID=" + DatabaseUserID +
";Password=" + DatabasePassword + ";";
/* Esquema do banco de dados */
public static int id_;
public static int id {
get { return ( id_ ); }
set { id_ = value; }
}
public static string modulo_;
public static string modulo {
get { return ( modulo_ ); }
set { modulo_ = value; }
}
public static string submodulo_;
public static string submodulo {
get { return ( submodulo_ ); }
set { submodulo_ = value; }
}
public static string acao_;
public static string acao {
get { return ( acao_ ); }
set { acao_ = value; }
}
public static string atributos_;
public static string atributos {
get { return ( atributos_ ); }
set { atributos_ = value; }
}
public static string log_;
public static string log {
get { return ( log_ ); }
set { log_ = value; }
}
// o campo data nao sera declarado pois nao ha necessidade
/**
* Metodo de conexao ao banco de dados
*/
public static void Conecta() {
metodo = "DB001 - Conecta()";
if( debug ) Console.WriteLine("Conectando ao banco de dados");
/**
Observacoes:
Por utilizar conexao ODBC que por sua vez e uma biblioteca externa ao mono deve-se
usar um Try/Catch para tratamento de erros. Estes erros serao direcionados ao
sistema de log de tela, ja que em caso de conexao indisponivel esta versao conta
apenas com registro em erros em tela
*/
try {
DBConnection = new MySqlConnection( connectionString );
DBConnection.Open();
erro = false;
if( debug ) Console.WriteLine("Conectado!");
}
catch {
Console.WriteLine("ODBCLog: Erro em sistema de log - " + metodo);
erro = true;
}
}
public static void Salva() {
// Monta comando SQL
DBCommand = DBConnection.CreateCommand();
DBCommand.CommandText = "INSERT INTO log VALUES (0," +
"'" + modulo + "'" +
", '" + submodulo + "'" +
", '" + acao + "'" +
", '" + atributos + "'" +
", '" + log + "'" +
", now() )";
if( debug ) Console.WriteLine("Salvando dados...");
DBCommand.ExecuteNonQuery();
Disconecta();
}
public static void Grava() {
Conecta();
Salva();
}
public static void Disconecta() {
if( debug ) Console.WriteLine("Disconectando...");
DBConnection.Dispose();
DBConnection = null;
}
}
