O namespace System.Diagnostics no .NET Framework possui vários e poderosos recursos para tracing, incluindo a principal API para este fim: a TraceSource. Como veremos, as APIs de tracing do System.Diagnostics podem ser estendidas. Neste artigo descreveremos cenários avançados e as várias maneiras para personalizar as APIs de tracing. Discutiremos também os princípios para design de APIs, incluindo herança, contenção e tipos genéricos.

TraceSource

Antes de analisarmos a personalização, vamos escrever um pequeno programa do tipo “Hello Word”, instrumentado com a API TraceSource:


            using System.Diagnostics;

            namespace ConsoleApplication1
            {
                class Program
                {
                    static TraceSource source1 = new TraceSource("MyProgram.Source1");
            
                    static void Main()
                    {
                        source1.TraceInformation("Main enters");
                        Console.WriteLine("Hello World");
                        source1.TraceInformation("Main exists");
                    }
                }
            }
        

Cada fonte de rastreamento contem uma instância de um SourceSwitch, que decide se uma mensagem de rastreamento deve ser emitida ou não. Cada fonte contém também um conjunto de instâncias do TraceListener, que determinam para onde as mensagens devem ser encaminhadas. Por padrão, as chaves do fonte não são configuradas para passar mensagens informativas, nem tampouco contêm os ouvintes (listeners) que passariam as mensagens para a console, portanto esse programa apenas imprime Hello Word.

Para permitir o rastreamento, o arquivo de configuração da Listagem 1 deve ser adicionado (clique com o botão direito no projeto e escolha Add>New Item>Application Configuration File).

Listagem 1. Adicionando um arquivo de configuração para rastreamento

            <configuration>
            <system.diagnostics>
                <sources>
                    <source name="MyProgram.Source1" switchName="MyProgram.Switch1">
                        <listeners>
                            <add name="Console" type="System.Diagnostics.ConsoleTraceListener"/>
                        </listeners>
                       </source>
                </sources>
                <switches>
                    <add name="MyProgram.Switch1" value="Information"/>
                </switches>
            </system.diagnostics>
        </configuration>
        

Esse arquivo de configuração instrui a chave do fonte para encaminhar as mensagens informativas e adiciona um ouvinte de rastreamento que encaminha as mensagens para a console. Agora, o programa não somente imprime Hello Word, como também rastreia mensagens simples quando o método principal entra e sai. Vejamos o resultado:


            MyProgram.Source1 Information: 0: Main enters

            Hello World
            
            MyProgram.Source1 Information: 0: Main exists
        

Agora que estamos familiarizados com alguns dos princípios básicos, iniciaremos a personalização desse código.

Criando um TraceListener personalizado

Começaremos desenvolvendo um ouvinte de rastreamento personalizado. Em vez de dirigir as mensagens de rastreamento para o console, o ouvinte de rastreamento enviará algumas mensagens importantes para um receptor de e-mail.

Ouvintes de rastreamento personalizados como esse, podem ser executados herdando diretamente da classe TraceListener. Mas por razões históricas, isso não é muito fácil. O TraceListener contém muitos membros virtuais interdependentes que podem ser sobrescritos. E após termos trabalhado com essas APIs por vários anos, ainda não sabemos que membros devem ser sobrescritos e em que cenários. Por esse motivo, criamos um tipo ajudante (helper type) chamado TraceListener2, que facilita a criação de ouvintes personalizados em cenários comuns.

Pensamos em utilizar um nome mais significativo, mas nos decidimos por TraceListener2. Esse nome adere a uma orientação que consta de uma atualização de orientações gerais do .NET Framework General Reference Design Guidelines for Class Library Developers em MSDN®online e estabelece que devemos “usar um sufixo numérico para indicar uma nova versão de uma API já existente, caso o nome da API seja o único nome que faz o sentido”.

Em geral, entretanto, prefiro usar um bom nome que descreva a diferença entre o tipo antigo e o novo. Poderíamos usar um identificador significativo, ao invés de adicionarmos um sufixo ou um prefixo.

O código da Listagem 2 mostra os membros principais de TraceListener2 (adicione uma nova classe em Add>New Item>Class).

Listagem 2. Adicionando uma nova classe

            public abstract class TraceListener2 : TraceListener {
                protected TraceListener2(string name): base(name)
                {
                }
            
                protected abstract void TraceEventCore(
                    TraceEventCache eventCache, string source,
                    TraceEventType eventType, int id,
                    string message);
            
                protected virtual string FormatData(object[] data)
                {
                    StringBuilder strData = new StringBuilder();
                    for (int i = 0; i < data.Length; i++) {
                        if (i >= 1) strData.Append("|");
                        strData.Append(data[i].ToString());
                    }
                    return strData.ToString();
                }
            
                protected void TraceDataCore(
                  TraceEventCache eventCache, string source,
                  TraceEventType eventType, int id,
                  params object[] data) {
                    if (Filter != null &&
                      !Filter.ShouldTrace(eventCache, source,
                        eventType, id, null, null, null, data))
                       return;
            
                    TraceEventCore(eventCache, source, eventType,
                        id, FormatData(data));
                }
            
                public sealed override void TraceEvent(
                    TraceEventCache eventCache, string source,
                    TraceEventType eventType, int id, string message)
                {
                    if (Filter != null &&
                        !Filter.ShouldTrace(eventCache, source,
                        eventType, id, message,null,null,null))
                        return;
            
                    TraceEventCore(eventCache, source, eventType, id, message);
                }
            
                public sealed override void Write(string message)
                {
                    if (Filter != null && !Filter.ShouldTrace(null, "Trace",
                        TraceEventType.Information, 0, message, null,
                        null, null))
                          return;
                        TraceEventCore(null, "Trace", TraceEventType.Information, 0, message);
                }
            
                public sealed override int GetHashCode()
                {
                    return base.GetHashCode();
                }
            
                public sealed override string Name
                {
                    get { return base.Name; }
                    set { base.Name = value; }
               }
            }
        

Como podemos ver, essa classe tem somente um membro abstrato, ficando bem claro o que deve ser sobrescrito.

Também, a maioria dos membros virtuais da classe base TraceListener são sobrescritos e selados, para indicar que não necessitam ser sobrescritos (note que não necessitam ser sobrescritos em cenários comuns. Há ainda cenários menos comuns em que isso pôde ser útil, mas como mencionamos antes, o TraceListener2 foi projetado como uma classe básica de ajuda para cenários comuns).

Há diversas orientações de projeto que levamos em conta quando tentamos melhorar o TraceListener. Em primeiro lugar, devemos definir os melhores cenários para uso em cada área principal. Depois, devemos fazer a maior sobrecarga virtual somente se a extensibilidade for requerida (algumas sobrecargas mais curtas devem ser chamadas de uma sobrecarga maior).

E, finalmente, devemos dar preferência à acessibilidade protegida sobre a acessibilidade pública para membros virtuais (os membros públicos devem fornecer a extensibilidade, caso requerido, chamando um membro virtual protegido).

Portanto, definimos o melhor cenário como aquele que executa um ouvinte personalizado para escrever eventos simples para um simples meio de armazenamento de eventos. Limitamos a quantidade de membros virtuais e tornamos protegidos todos os pontos restantes de extensibilidade.

A criação de um ouvinte personalizado para cenários simples agora é trivial. A única coisa requerida é executar o método abstrato TraceEventCore.

Note que o Visual Studio 2005 fornece uma nova e grande característica que facilita ainda mais: apenas criarmos uma classe vazia que herda de uma classe abstrata, então clicamos de direita no nome da classe abstrata e selecionamos Implement Abstract Class. O editor gerará implementações padrão para todos os membros abstratos, como o código da Listagem 3.

Listagem 3. Implementação dos membros estáticos

            public class EmailListener: TraceListener2
            {
                protected override void TraceEventCore (
                    TraceEventCache eventCache, string source,
                    TraceEventType eventType,  int id,
                    params object[] data)
                    throw new System.Exception ("The method or operation is not…")
            }
        

Só resta adicionar os construtores e preencher o corpo do método autogerado (sobrescrito). O exemplo mostrado na Listagem 4, representa um ouvinte personalizado responsável por enviar todos os eventos críticos via e-mail.

Listagem 4. Enviando e-mail para os eventos

            public class EmailListener1 : TraceListener2
            {
                public EmailListener1(string name) : base(name)
                {
                      
                }
            
                public EMailListener1(): this("EMailListener")
                {
            
                }
            
                protected override void TraceEventCore(
                    TraceEventCache eventCache,
                    string source, TraceEventType eventType,
                    int id, string message)
                {
            
                    if (eventType != TraceEventType.Critical)
                        return;
                    
                    MailMessage emailMessage = new MailMessage("kcwalina@microsoft.com",
                        "kcwalina@microsoft.com");
            
                    emailMessage.Body = message;
                    emailMessage.Subject = "Critical Error";
                    client.Send(emailMessage);
                 }
            }
        

Para usar o ouvinte, temos que configurar o SMTP no arquivo de configuração. Algo como o código da Listagem 5, em que basta preencher os valores do hospedeiro, do username e da senha.

Listagem 5. Alterando o arquivo de configuração para envio de e-mail

            <system.net>
            <mailSettings>
                <smtp deliveryMethod="network">
                    <network host="..." defaultCredentials="false"
                        userName="..." password="..." />
        
                </smtp>
            </mailSettings>
        </system.net>
        

Podemos agora adicionar o ouvinte à fonte de rastreamento, conforme o código da Listagem 6.

Listagem 6. Ouvinte de rastreamento

            <listeners>
            <add name="Email"
                type="Microsoft.Samples.Tracing.EMailListener,Extensions"/>
            <add name="Console"
                type="System.Diagnostics.ConsoleTraceListener"/>
        </listeners>
        

Agora, tentamos gerar algumas mensagens críticas de rastreamento, tal como o seguinte código:


            static void Main() {
                source1.TraceInformation("Main enters");
                Console.WriteLine("Hello World");
                source1.TraceEvent(TraceEventType.Critical, 0, "Main exists");
              }
        

Criando uma chave personalizada

A criação de ouvintes personalizados é um cenário relativamente comum e está bem documentado. O mesmo acontece com chaves personalizadas, mas em menor grau. O Framework tem uma classe chave base chamada System.Diagnostics.Switch e três subclasses: BooleanSwitch, TraceSwitch e SourceSwitch.

As primeiras duas chaves concretas pretende-se que sejam usadas como tipos autônomos, e a criação de uma chave autônoma personalizada é relativamente simples. A SourceSwitch pretende-se que seja usada com classes TraceSource, e executar um SourceSwitch personalizado é um bocado mais complicado. Discutiremos esse assunto logo adiante.

Estendendo Chaves

O TraceSource usa a SourceSwitch, que foi adicionada ao .NET Framework 2.0, para decidir que mensagens devem ser rastreadas. As classes Trace e TraceSwitch, que constavam da primeira versão do Framework, ainda estão disponíveis. Sendo mais simples, as usaremos por enquanto para ilustrar a criação de chaves personalizadas.

Vamos começar criando uma chave personalizada que se ajusta melhor do que TraceSwitch para rastrear o fluxo da implementação (entrada e saída do método). Neste exemplo, se usássemos as classes Trace e TraceSwitch para rastrear o fluxo da execução, teríamos que escrever algo parecido com o código da Listagem 7.

Listagem 7. Código usando as classes Trace e TraceSwitch

            static TraceSwitch trace = new TraceSwitch(
                "flowSwitch");
              
              ...
              
              void Main(){
                  if(trace.TraceInformation){
                      Trace.WriteLine("Entering method Foo");
                  }
              
                  ...
              
                  if(trace.TraceInformation){
                      Trace.WriteLine("Exiting method Foo");
                  }
              }
        

Mas isso tem algumas limitações. E se quisermos habilitar qualquer uma dessas mensagens independentemente? Ou seja, se quiséssemos emitir somente a mensagem de saída? Possivelmente desejaríamos uma chave personalizada com níveis personalizados correspondentes a eventos de entrada e saída. Algo como o código da Listagem 8.

Listagem 8. Personalizando as mensagens

            static FlowSwitch trace = new FlowSwitch(

                "flowSwitch");
              
              ...
              
              void Main(){
                  if(trace.Entering)
                  {
                     Trace.WriteLine("Entering method Foo");
                  }
              
                  ...
              
                  if(trace.Exiting)
                  {
                     Trace.WriteLine("Exiting method Foo");
                  }
              }
        

Tal FlowSwitch pode de fato ter quatro estados: um indica que os eventos devem ser rastreados ao entrar no método, outro ao sair do método, sendo que o terceiro e o quarto constituem combinações dos valores básicos para rastrear ambos ou nenhum dos tipos de evento.

Os quatro estados de um FlowSwitch são representados pela enumeração FlowSwitchSettings, como o seguinte código:


            [Flags]
            public enumeração FlowSwitchSettings
            {
              None = 0, Entering = 32, Exiting = 64,
              Both = Entering | Exiting
            }
        

Valores que representam combinações dos flags são muito convenientes e fornecer tais valores é recomendado pelas orientações de projeto. Note que a enumeração foi nomeada usando uma frase de substantivo no plural e foi aplicado o FlagsAttribute. Isso segue duas orientações fundamentais de projeto de enumeração dos flags: aplicar o System.FlagsAttribute às enumerações de flags mas não às enumerações simples e usar um nome de tipo plural para a enumeração com campos bit para os valores (chamados também de enumeração de flags).

O FlowSwitch usa essa enumeração para a propriedade Level, que é configurada e alterada pelo método OnValueChanged. O método é chamado depois que a propriedade protegida Value é configurada pelo valor armazenado no arquivo de configuração, o que acontece normalmente quando a chave é instanciada.

O valor pode ser atualizado mais tarde durante a implementação do programa chamando o método Trace.Refresh (novo no .NET Framework 2.0), que relê o arquivo de configuração. Adicionamos também duas propriedades de ajuda (Entering e Exiting), para evitar ter que fazer operações bitwise complexas em cenários de uso comuns (Listagem 9).

Listagem 9. Classe FlowSwitch

            public class FlowSwitch : Switch
            {
                public FlowSwitch(string name) : base(name, "") {}
            
                public FlowSwitchSettings Level
                {
                    get { return (
                      FlowSwitchSettings)this.SwitchSetting; }
                    set { this.SwitchSetting = (int)value; }
                }
            
                public bool Entering
                {
                    get
                    {
                        return (Level &
                          FlowSwitchSettings.Entering) == FlowSwitchSettings.Entering;
                    }
                }
            
                public bool Exiting
                {
                    get
                    {
                        return (Level &
                          FlowSwitchSettings.Exiting) == FlowSwitchSettings.Exiting;
                    }
                }
            
                protected sealed override void OnValueChanged()
                {
                    SwitchSetting = (int)Enum.Parse(typeof(FlowSwitchSettings),
                      Value);
                }
            }
        

O TraceSwitch e o BooleanSwitch (que são incluídos no .NET Framework ) são outros dois exemplos de implementações concretas da classe Switch.