Estendendo o System.Diagnostics - Parte II

Veja nesse artigo, como estender o namespace System.Diagnostics.

Estendendo o System.Diagnostics – Parte II

Publicado originalmente na MSDN Magazine USA nr 21.

Estendendo o SourceSwitch

Como podemos ver, estender a classe Switch é relativamente simples. Estender o SourceSwitch é mais interessante e desafiante. Como mencionamos antes, a chave é usada com o ponto de entrada principal de rastreamento chamado TraceSource.

O TraceSource se baseia em duas enumerações: uma é usada para especificar o tipo do evento que está sendo rastreado e a segunda especifica os tipos de eventos que a fonte dada deve rastrear. O desafio de executar chaves de fonte personalizadas vem do fato de que as enumerações necessitam ser especialmente projetados para interoperar corretamente quando usadas dentro de um TraceSource.

Por exemplo, no .NET Framework, o TraceSource usa uma enumeração dos flags SourceLevels para representar as configurações de Switch e uma enumeração simples TraceEventType (não flag) para representar os tipos de evento, como mostrado a seguir:

 

TraceSource diagnostics = new TraceSource(

  "MyApplication");

// diagnostics.Switch.Level = SourceLevels.Error;

diagnostics.TraceEvent(TraceEventType.Error, 0,

  "File not found");

 

Para a chave de fonte de fluxo personalizada, reusamos a enumeração FlowSwitchSettings da seção anterior, para representar as configurações da chave e para adicionar uma enumeração nova. FlowEventType; para representar o tipo de um evento. Essa chave de fluxo pode controlar dois tipos de eventos: Entering e Exiting.

Conseqüentemente, a enumeração terá dois valores. Note que essa enumeração não é uma enumeração dos flags. Cada evento pode ser um evento de entrada ou de saída, mas não ambos:

 

public enum FlowEventType

{

Entering = 32,

Exiting = 64

}

 

Podemos observar, que esse nome de enumeração (FlowEventType), é uma frase de um substantivo singular de acordo com orientação básica de nomeação para a enumeração: usar um nome de tipo singular para a enumeração a menos que seus valores sejam campos bit.

Ao contrário do FlowSwitch, o FlowSourceSwitch (e todos os SourceSwitches), não necessitam das propriedades de ajuda (Entering e Exiting). Ao invés disso, contém um método ShouldTrace que compara o tipo do evento com a configuração da chave, para determinar se o evento deve ser rastreado ou não (Listagem ).

 

Listagem 10. Para rastrear ou não o evento

public class FlowSourceSwitch1 : SourceSwitch {

public FlowSourceSwitch1(string name) : base(name)

{

 

}

 

public new FlowSwitchSettings Level {

  get { return (FlowSwitchSettings)base.Level; }

  set { base.Level = (SourceLevels)value;  }

}

 

public bool ShouldTrace(FlowEventType eventType) {

  return ((int)eventType & (int)Level) != 0;

}

 

protected sealed override void OnValueChanged() {

  SwitchSetting =

    (int)Enum.Parse(typeof(FlowSwitchSettings),

Value);

}

}

 

O método ShouldTrace é provavelmente o membro mais interessante de uma chave de fonte. Lembremo-nos que foi mencionado que os valores inteiros das duas enumerações usadas pelos fontes de rastreamento, tem que ser escolhidos com cuidado. Isso é devido à implementação do método ShouldTrace que executa uma comparação bit a bit dos valores inteiros, para determinar se um evento deve ser rastreado. Sendo o método SourceSwitch.ShouldTrace não virtual, não podemos mudar à forma como os valores são comparados.

 

Podemos apenas mudar, ou melhor, selecionar os valores com cuidado para personalizar o funcionamento do método. Note que o método é não virtual para maximizar o desempenho (permite o inlining).

O desempenho desse método é crítico para garantir que o processamento extra do rastreamento se aproxime de zero quando uma chave for desligada e a verificação da chave for levantada. Por exemplo, se a verificação puder ser inlined, e se a expressão booleana for falsa, o custo da declaração inteira será desprezível:

 

If (source.Switch.ShouldTrace(TraceEvent.Information))

source.TraceEvent(TraceEvent.Information,…);

 

De fato, não há nenhuma necessidade de adicionar o método ShouldTrace a uma chave personalizada, afinal, o método ShouldTrace é apenas uma sobrecarga e não é chamado por fontes de rastreamento. O TraceSource é que chama o método do tipo base ShouldTrace(TraceEventType) de qualquer maneira.

A sobrecarga é útil para os cenários em que o chamador quer retirar a verificação da chave para fora da chamada ao TraceEvent, visando melhorar o desempenho. Mais adiante, mostraremos como engatar um SourceSwitch personalizado a um TraceSource. Mas antes vamos ver se podemos fazer melhor do que o tão cuidadosamente elaborado FlowSourceSwitch.

SourceSwitch Genérico

A personalização ou extensibilidade, podem ser fornecidos via diversos mecanismos incluindo abstrações, eventos e callbacks. O .NET Framework 2.0 adicionou uma característica chamada Generics, que é um mecanismo muito poderoso de estensibilidade. A Listagem 11 mostra como o generics pode ser usado para fornecer uma chave que pode ser ajustada para suportar configurações personalizadas.

 

Listagem 11.  Switch com suporte a configurações

public class SourceSwitch

  TSetting>: SourceSwitch

{

TSetting enumLevel;

static Type settingEnumType = typeof(TSetting);

public SourceSwitch(string name) : base(name) {}

 

public new TSetting Level {

get { return enumLevel; }

set {

enumLevel = value;

SwitchSetting = Convert.ToInt32(enumLevel);

}

}

 

public bool ShouldTrace(TEventType eventType)

{

int eventTypeValue = Convert.ToInt32(

eventType);

return (eventTypeValue & SwitchSetting) != 0;

}

 

protected override void OnValueChanged()

{

Level = (TSetting)Enum.Parse(settingEnumType,

this.Value);

}

}

 

Esse tipo genérico tem dois tipos de parâmetros: o TEventType e o TSetting, que são usados a fim especificar as enumerações que representam o tipo do evento e a configuração da chave. Essa configuração da chave pode ser usada de uma maneira que torna muito mais simples a criação de chaves personalizadas:

 

public class FlowSourceSwitch: 

  SourceSwitch

{

public FlowSourceSwitch(string name):base(name) {}

}

 

O Generics suporta uma característica chamada restrições (constraints). Essa característica permite que os parâmetros de tipo sejam restringidos para determinados tipos. Por exemplo, é possível especificar que o argumento de tipo passado a TSetting, tem que ser derivado de, por exemplo, IComparable.

Neste exemplo particular do Switch, seria bom restringir ambos os parâmetros de tipo para os tipos que herdam de System.Enum. Infelizmente, tal restrição não é suportada pelo .NET Framework 2.0.

Essa limitação pode ser parcialmente minimizada pela escolha de nomes de parâmetros de tipo descritivos. Ao nomearmos os parâmetros de tipo, seguimos a orientação de uso de nomes descritivos, a menos que um nome single-letter seja auto-explicativo e um nome descritivo não adicione valor.

Criando um TraceSource personalizado

Então, como integrar a nova chave a um TraceSource? Vejamos duas abordagens. Na primeira, herdaremos de TraceSource e na segunda, usaremos contenção. Primeiramente, herdamos de TraceSource para personalizar o tipo switch padrão, conforme a Listagem 12.

 

Listagem 12.  Personalizando o tipo switch

public class FlowTraceSource1 : TraceSource

{

public FlowTraceSource1(string name) : base(name)

{

base.Switch = new FlowSourceSwitch(name);

}

 

public new FlowSourceSwitch Switch

{

get { return (FlowSourceSwitch)base.Switch; }

set { base.Switch = value; }

}

}

 

Se a fonte for configurada usando o sistema de configuração, o tipo switch deve ser especificado no arquivo de configuração da fonte, conforme a Listagem 13.

 

Listagem 13. Tipo deve ser especificado arquivo de configuração

"<source"

  name="FlowTracing"

  switchName="FlowTracing"

  switchType="Microsoft.Samples.Tracing.FlowSourceSwitch,Extensions">

  <listeners>

    ...

  listeners>

"source>"

 

Agora a fonte pode ser usada, como mostrado no seguinte exemplo:

 

static readonly FlowTraceSource1 flow1 =

new FlowTraceSource1("FlowTracing");

flow1.TraceEvent((TraceEventType)

  FlowEventType.Entering, 0, "Method1");

flow1.TraceEvent((TraceEventType)

  FlowEventType.Exiting, 0, "Method1");

 

Fazer o cast do argumento de tipo de evento não é conveniente. O problema pode ser solucionado adicionando-se (aos métodos de TraceEvent) sobrecargas de FlowEventType, como mostrado na Listagem 14.

 

Listagem 14. Solucionando o problema de cast do argumento

public class FlowTraceSource2 : TraceSource

{

  ...

  public void TraceEvent(FlowEventType eventType,

int id, string format, params object[] args)

  {

    int eventTypeValue = Convert.ToInt32(eventType);

base.TraceEvent((TraceEventType)eventTypeValue,

id, format, args);

  }

 

  public void TraceEvent(FlowEventType eventType,

int id, string format)

  {

int eventTypeValue = Convert.ToInt32(eventType);

base.TraceEvent((TraceEventType)eventTypeValue,

id, format);

  }

}

 

Temos, porém, um problema com esse projeto. O FlowTraceSource simula justamente um TraceSource estendido. O FlowTraceSource tem todos os membros herdados de TraceSource e de fato podemos fazer o cast para TraceSource e usá-lo no seu lugar.

O problema é que FlowTraceSource não é realmente um TraceSource. Por alguma razão, não foi projetado para rastrear eventos normais de TraceSource, tal como TraceEvent.Information, pois o FlowSourceSwitch não foi realmente projetado para controlar tais eventos. Nos casos onde uma subclasse não pode ser usada no lugar de sua classe base, a herança não deve ser usada. A contenção pode ser uma abordagem melhor.

Contenção

A extensibilidade via contenção (envelopamento), significa que um tipo personalizado usa outro tipo na sua implementação ao invés de herdar do tipo. Neste exemplo, FlowTraceSource3 usa TraceSource na sua implementação (Listagem 15).

 

Listagem 15. Envelopando um tipo

public class FlowTraceSource3

{

TraceSource source;

public FlowTraceSource3(string name)

{

source = new TraceSource(name);

source.Switch = new FlowSourceSwitch(name);

}

 

public FlowSourceSwitch Switch

{

  get { return (FlowSourceSwitch)source.Switch;

}

set { source.Switch = value; }

}

 

public void TraceEvent(FlowEventType eventType,

int id, string format, params object[] args)

{

int eventTypeValue = Convert.ToInt32(eventType);

source.TraceEvent((TraceEventType)

eventTypeValue, id, format, args);

}

 

public void TraceEvent(FlowEventType eventType, int id,

string format)

{

int eventTypeValue = Convert.ToInt32(eventType);

source.TraceEvent((TraceEventType)eventTypeValue,

id, format);

}

 

public TraceListenerCollection Listeners

{

get { return source.Listeners; }

}

 

public void Flush() { source.Flush(); }

 

public void Close() { source.Close(); }

}

 

Esse projeto parece muito mais simples e mais limpo. O FlowTraceSource3 não simula ser um substituto do TraceSource, porém ao usar TraceSource internamente, o FlowTraceSource3 obtém muitas das propriedades úteis de TraceSource. Por exemplo, o FlowTraceSource3 pode ser configurado usando a sistema de configuração, re-utiliza a coleção de ouvintes de TraceSource com suas facilidades de sincronização, e muitas outras.

Da mesma forma que o Switch genérico do qual falamos anteriormente, é possível criar uma fonte genérica de rastreamento. O tipo tem dois parâmetros de tipo usados para especificar o tipo da chave e o tipo do tipo do evento de enumeração. Mais uma vez, o tipo de evento não pode ser restrito, mas a chave pode ser (Listagem 16).

 

Listagem 16. TraceSource genérico

public class TraceSource

  where TSwitch : SourceSwitch

{

   TraceSource source;

   public TraceSource(string name)

   {

     source = new TraceSource(name);

Type st = typeof(TSwitch);

object s = st.GetConstructor(

  new Type[] { typeof(string) }).Invoke(

new object[] { name });

this.Switch = (TSwitch)s;

   }

 

   public TSwitch Switch

   {

get { return (TSwitch)source.Switch; }

set { source.Switch = value; }

   }

 

   public void TraceEvent(TEventType eventType,

int id, string format, params object[] args)

   {

int eventTypeValue = Convert.ToInt32(eventType);

source.TraceEvent((TraceEventType)eventTypeValue,

id, format, args);

}

 

public void TraceEvent(TEventType eventType,

int id, string format)

{

int eventTypeValue = Convert.ToInt32(eventType);

source.TraceEvent((TraceEventType)

eventTypeValue, id, format);

}

 

public void Flush() { source.Flush(); }

 

public void Close() { source.Close(); }

}

 

Esse tipo genérico pode ser usado como a seguir:

 

public class FlowTraceSource:

  TraceSource

{

public FlowTraceSource(string name) : base(name) { }

}

Estensibildade do sistema de configuração

Voltemos ao ouvinte de e-mail. Lembramos que o endereço do recipiente de e-mail, o assunto e tudo o mais foi hardcoded. Mas existe uma maneira melhor de fazermos isso. Podemos permitir que o ouvinte seja configurável usando o sistema de configuração e pode ser configurado usando a seção System.Diagnostics do arquivo de configuração.

Para fazer isso, necessitamos sobrescrever o método GetSupportedAttributes e usar o dicionário Attributes ao invés dos valores hardcoded ao enviar mensagens de e-mail.  A Listagem 17 mostra o código usado para fazer isto.

 

Listagem 17. Sobrescrevendo o método GetSupportedAttributes

public class CriticalMailListener: TraceListener2

{

public CriticalMailListener() :

base("CriticalMailListener") { }

public CriticalMailListener(string name):

base(name) { }

 

protected override string[]

GetSupportedAttributes()

{

return new string[] { "Receipients",

"Subject", "From" };

}

 

protected override void TraceEventCore(

  TraceEventCache eventCache, string source,

TraceEventType eventType, int id, string message)

{

if (eventType != TraceEventType.Critical)

return;

MailMessage emailMessage = new MailMessage(

Attributes["From"], Attributes["Recipients"]);

emailMessage.Body = message;

emailMessage.Subject = Attributes["Subject"];

SmtpClient client = new SmtpClient();

client.Send(emailMessage);

}

}

 

Agora o ouvinte pode ser configurado conforme o código da Listagem 18.

 

Listagem 18. Alterando o arquivo de configuração

<sharedListeners>

  <add name="Email"

  type="Microsoft.Samples.Tracing.CriticalMailListener,Extensions"

  Recipients="kcwalina@microsoft.com"

  Subject="Critical Error from TraceListener"

  From="kcwalina@microsoft.com "/>

sharedListeners>

Conclusão

Como podemos ver, as APIs de rastreamento do System.Diagnostics suportam extensibilidades poderosas. Mais detalhes sobre as APIs de rastreamento podem ser encontrados no meu blog Tracing APIs in .NET Framework 2.0.

 

Krzysztof Cwalina é gerente de programa no time de CLR da Microsoft. Começou a carreira como Microsoft API artificiosos na primeira liberação do .NET Framework. Atualmente, conduz o esforço para desenvolver, promover e aplicar as diretrizes de desígnio no .NET Framework e WinFx. Localize-o no blog: blogs.msdn.com/kcwalina.

 

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados