Jednoduchý exception logging pomocí Trace/TraceSource mechanizmů .NET

Pokud se sháníte po jednoduchém mechanizmu, jak ve Vašich aplikacích logovat výjimky, pak zřejmě v .NET pro consolové/WinForm aplikace marně hledáte něco, jako je healthMonitoring pro ASP.NET, kde je připraven jednoduchý, ale mocný nástroj a lze pomocí pouhých několika řádků ve web.config souboru získat přehled o výjimkách ve Vaší aplikaci, např.:

<configuration>
    <system.web>
        <healthMonitoring enabled="true">
            <rules>
                <add name="Mail Notifications on All Errors" eventName="All Errors" provider="SimpleMailWebEventProvider" profile="Default"/>
            </rules>
            <providers>
                <add name="SimpleMailWebEventProvider" type="System.Web.Management.SimpleMailWebEventProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" from="errors@havit.cz" to="errors@example.cz" subjectPrefix="MyApplication: " buffer="true" bufferMode="Notification" maxEventLength="4096" maxMessagesPerNotification="2"/>
            </providers>
        </healthMonitoring>
    </system.web>
</configuration>

…výše uvedená konfigurace Vám zajistí zasílání mailových notifikacích o neobsloužených výjimkách a chyách Vaší aplikace, jednoduché, prosté.

Jak na to u consolových a WinForm aplikací?

Pro consolové či WinForm aplikace nic jako healthMonitoring připraveno není, nicméně s pár řádky kódu lze dosáhnout obdobného efektu a dokonce u toho využít široké možnosti pro tracing v .NET 2.0. Základní myšlenka je prostá:

  • .NET 2.0+ v sobě obsahuje široké možnosti pro tracing, kdy můžeme definovat různé zdroje zpráv (TraceSource), zprávy různé rozdělovat (Switch), filtrovat (TraceFilter) a směrovat na různé listenery (výstup do konzole, XML, textového souboru, event-logu, SQL, SMTP, atd. další si můžeme napsat) – to vše bez nutnosti jakýchkoliv doplňků – ať už Microsoftích Enterprise Library / Application Blocků, či produktů třetích stran typu Log4Net. Základní přehled o možnostech si můžeme udělat například z článku A Tracing Primer – Part I [Mike Rousos].
  • .NET sám nemá mechanizmus, jak výjimky zapisovat do trace
  • napsat pár řádků kódu, které zajistí zápis výjimek do trace je snadné!!!
  • do trace můžeme buď výjimky posílat explicitně (níže volání ExceptionTracer.TraceException(myException)),
  • nebo se můžeme přihlásit k odběru událostí, které neošetřené výjimky způsobují,
    • obecně jde o událost AppDomain.CurrentDomain.UnhandledException
    • ve WinForm aplikacích pak událost Application.ThreadException

Interface pro nasazení?  …prostý

V nejjednodušší variantě si můžeme udělat například třídu ExceptionTracer, kterou přihlásíme k odběru příslušných událostí a v obsluze událostí už budeme jen výjimku posílat do trace.

Pro konzolové aplikace:

class Program
{
    static void Main(string[] args)
    {
        ExceptionTracer.SubscribeToUnhandledExceptions();

        throw new InvalidOperationException("Chybka!");

        // ExceptionTracer.Default.TraceException(new ArgumentNullException("param", "Sakrapísek!"));
    }
}

Pro WinFormAplikace:

static class Program
{
    [STAThread]
    static void Main()
    {
        ExceptionTracer.SubscribeToWindowsFormsThreadExceptions();

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

App.config pak může vypadat nějak takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <sources>
            <source name="Exceptions" switchValue="Error">
                <listeners>
                    <add name="LogFileListener"
                        type="System.Diagnostics.TextWriterTraceListener"
                         initializeData="Exceptions.log"
                    />
                    <add name="XmlListener"
                         initializeData="Exceptions.xml"
                         type="System.Diagnostics.XmlWriterTraceListener"
                    />
                </listeners>
            </source>
        </sources>
    </system.diagnostics>
</configuration>

ExceptionTracer [Simplified Version]

Jednoduchá, ale plně funkční verze ExceptionTracekru může vypadat třeba takto:

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;

namespace Havit.Diagnostics
{
    public class ExceptionTracer
    {
        /// <summary>
        /// Přihlásí ExceptionTracer k odběru všech neobsloužených výjimek (event AppDomain.CurrentDomain.UnhandledException).
        /// </summary>
        public static void SubscribeToUnhandledExceptions()
        {
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }
        private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.ExceptionObject is Exception)
            {
                TraceException((Exception)e.ExceptionObject);
            }
        }

        /// <summary>
        /// Přihlásí ExceptionTracer k odběru všech neobsloužených výjimek WinForm (event Application.ThreadException).
        /// </summary>
        public static void SubscribeToWindowsFormsThreadExceptions()
        {
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
        }
        private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            TraceException(e.Exception);

            // původní implementace obsluhy výjimky, WinForm dialog s chybou
            using (ThreadExceptionDialog excptDlg = new ThreadExceptionDialog(e.Exception))
            {
                DialogResult result = excptDlg.ShowDialog();
                if (result == DialogResult.Abort)
                {
                    Application.Exit();
                }
            }
        }

        /// <summary>
        /// Pošle do trace zadanou výjimku.
        /// </summary>
        /// <param name="exception">výjimka k zaznamenání</param>
        public static void TraceException(Exception exception)
        {
            TraceSource ts = new TraceSource("Exceptions");

            ts.TraceEvent(TraceEventType.Critical, 0, exception.ToString());

            ts.Flush();
            ts.Close();
        }
    }
}

Příklad výstupu [XML]

<E2ETraceEvent xmlns="http://schemas.microsoft.com/2004/06/E2ETraceEvent">
<System xmlns="http://schemas.microsoft.com/2004/06/windows/eventlog/system">
  <EventID>0</EventID> 
  <Type>3</Type> 
  <SubType Name="Critical">0</SubType> 
  <Level>1</Level> 
  <TimeCreated SystemTime="2008-01-31T21:08:35.0625000Z" /> 
  <Source Name="Exceptions" /> 
  <Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" /> 
  <Execution ProcessName="WindowsFormsApplication1" ProcessID="1232" ThreadID="1" /> 
  <Channel /> 
  <Computer>OSKAR</Computer> 
</System>
<ApplicationData>System.ApplicationException: Test !!! at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in D:\Development\ExceptionLogging\WindowsFormsApplication1\Form1.cs:line 20 at System.Windows.Forms.Control.OnClick(EventArgs e) at System.Windows.Forms.Button.OnClick(EventArgs e) at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent) at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks) at System.Windows.Forms.Control.WndProc(Message& m) at System.Windows.Forms.ButtonBase.WndProc(Message& m) at System.Windows.Forms.Button.WndProc(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m) at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)</ApplicationData> 
</E2ETraceEvent>

Použití C# 3.0 pro runtime .NET Framework 2.0

Pokud vyvíjíte aplikaci ve Visual Studiu 2008 a aplikace je určena pro runtime .NET Framework 2.0, nic vám nebrání použít nové charakteristiky jazyka C# 3.0. Úžasné je, že pro to nemusíte udělat vůbec nic.
Není možné pochopitelně použít ty rysy jazyka, které vyžadují knihovny instalované .NET Frameworkem 3.5. Můžete využít například eleganci lambda výrazů nebo zkráceného zápisu vlastností, zapomeňte ale na LINQ.

PAUSE v Powershellu

Příkaz PAUSE známý z BAT souborů v PowerShellu nefunguje, pokud chceme do cmdletu dostat vyčkání na stisk klávesy, můžeme například použít metodu ReadKey:

$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")Je i mraky jiných možností, jak dosáhnout efektu obdobného PAUSE.

Jak přepsat PageStatePersister

 

Jak již bylo napsáno v jiném našem článku, vlastnost PageStatePersister slouží k získání objektu, který zajišťuje uložení ViewState. Článek detailně popisuje použití třídy SessionPageStatePersister, která zajišťuje uložení ViewState do Session.

Chybná implementace

Pokud chceme použít SessionPageStatePersister bez konfigurace pomocí adaptéru, musíme přepsat zmíněnou vlastnost, ideálně ve společném předkovi všech stránek v aplikaci. Nejjednodušší (avšak nesprávná implementace) může při každém dotazu na hodnotu vlastnosti vyrobit instanci persisteru a tu vrátit.

protected override PageStatePersister PageStatePersister
{
    get
    {
        // takto ne!
        new SessionPageStatePersister(this.Page);
    }
}
Symptomy

Dosáhneme sice požadovaného efektu – ViewState se ukládá do Session, ovšem v dříve funkčních částech aplikace začneme pozorovat nečekané efekty:

  1. Mezi postbacky se nezachovávají některé hodnoty ukládané do control state (například DataKeys u GridView).
  2. V některých velmi specifických případech se neprovede nastavení hodnoty do vlastnosti, pokud tato vlastnost již měla hodnotu nastavenou v souboru ASPX a hodnota vlastnosti se vnitřně ukládala do ViewState (po provedení příkazMujControl.Vlastnost = novaHodnota nebude hodnota vlastnosti změněna).
Správná implementace

Ačkoliv se to nedočteme v dokumentaci, ale zjistíme po bližším zkoumání .NET reflektorem, musíme v jedné instanci třídy Page vracet stále stejnou instanci, protože na hodnotu vlastnosti se opakovaně dotazují metody třídy Page (jednou z nich je metoda RegisterRequiresControlState, což by vysvětlovalo symptomy v bodu 1). Po zapracování tohoto poznatku jsme se zmíněných problémů zbavili. Správně je tedy třeba vlastnost PageStatePersister přepsat takto:

protected override PageStatePersister PageStatePersister
{
    get
    {
        if (_pageStatePersister == null)
        {
            _pageStatePersister = new SessionPageStatePersister(this.Page);
        }
        return _pageStatePersister;
    }
}
private PageStatePersister _pageStatePersister;

Ctrl+Shift+Esc spustí Task Manager

…příjemná klávesová zkratka, o které jsem nevěděl.

Zjištění volné paměti počítače na webhostingu

Pokud se chceme z naší hostované ASP.NET aplikace dozvědět něco o stroji, na kterém běží, nemůžeme tak zpravidla udělat přes WMI dotazy, či performance-countery, protože nemáme k dispozici účet s dostatečnými právy a dostáváme výjimku Access Denied.

I nám C#-istům je však k dispozici to, co mají ve VisualBasicu ukryto pod featurou My, konkrétně nás zde zajímá My.Computer.Info, které není nic jiného než instancí třídyMicrosoft.VisualBasic.Devices.ComputerInfo s propertama AvailablePhysicalMemory,AvailableVirtualMemory, TotalPhysicalMemory, TotalVirtualMemory a několika dalšími s informacemi o verzi OS atp.

Stačí si tedy do našeho C# projektu nareferencovat assembly Microsoft.VisualBasic, vytvořit si instanci třídy ComputerInfo a zeptat se jí na přísušné hodnoty:

using Microsoft.VisualBasic.Devices;

...

ComputerInfo computerInfo = new ComputerInfo();
InfoLb.Text = computerInfo.AvailablePhysicalMemory.ToString("n0");

Třída ComputerInfo používá volání podpůrných nativních metod .NET a informace o paměti stroje dostaneme i pod běžným hostingovým účtem.

Změna výchozí verze ASP.NET pro nové weby IIS

Pokud Vám IIS pro nově zakládané web-site přednastavuje jinou verzi ASP.NET, než si představujete, můžete toto výchozí nastavení ovlivnit spuštěním příkazu aspnet_regiis.exe od nově požadované verze ASP.NET s níže uvedenými parametry:

aspnet_regiis -sn W3SVC/

Utilitu aspnet_regiis najdete v C:\WINDOWS\Microsoft.NET\Framework\<číslo verze>\.

ViewState vs. fáze Init, aneb jak jsem se chytil

Na svém posledním ASP.NET kurzu jsem se báječně chytil na jednom primitivním demu na fungování ViewState. Krásně jsem předváděl, jak „ve fázi Init není ještě ViewState trackován, po fázi Init nastává LoadViewState a před tím se zapne jeho tracking – TrackViewState()“, nu což, vyrobil jsem si krásné demo a nestíhal jsem pak chvíli zírat.

(Základy fungování ViewState viz článek Co by měl každý vědět o ViewState.)

„Nefunkční“ demo

Vyrobil jsem primitivní stránku, s jedním Labelem a jedním tlačítkem.

<asp:Label ID="MyLabel" Text="Výchozí text" runat="server" />
<asp:Button ID="MyButton" Text="Postback" runat="server" />

a jak jsem se demonstrovat, jak přiřazení vlastnosti Text ve fázi Init nepřežije postback:

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        MyLabel.Text = "Nová hodnota z fáze Init!";
    }
}

Uff, jaké bylo mé překvapení, když Label i po kliknutí na tlačítko Postback přežíval s hodnotou „Nová hodnota z fáze Init!“. Mnohým je hned jasné, kde jsem udělal chybu, ostatním přiblížím:

ViewState v životním cyklu stránek/controlů

Mé překvapení naštěstí netrvalo dlouho, rychle jsem si ověřil, kde přesně se volá v životním cyklu controlů TrackViewState() a rozuzlení bylo na světě:

  1. TrackViewState() není první operace po fázi Init, nýbrž poslední operace fáze Init samotné,
  2. fáze Init (jako jediná ze základních fází životního cyklu) probíhá zdola nahoru, procházením stromu control-tree do hloubky – fáze Init nadřazeného controlu je završena až po dokončení fází Init všech jeho child-controls.

Výsledek je tedy nasnadě, v obsluze události Page_Init jsme úplně na vrcholu control-tree a fáze Init jednotlivých controlů již proběhla. Jejich TrackViewState() již proběhl a ViewState controlů tak již sleduje změny jednotlivých hodnot. Nastavení MyLabel.Text ve fází Page_Init tedy již je operací, kdy změny ViewState labelu jsou sledovány, narozdíl od ViewState stránky (Page) samotné, kde ještě TrackViewState() neproběhl.

„Funkční“ demo

Demo jsem rychle opravil a vše krásně fungovalo, jak má:

void MyLabel_Init(object sender, EventArgs e)
{
    if (!Page.IsPostback)
    {
        this.Text = "Text nastavený ve fázi Init labelu, nepřežije Postback.";
    }
}

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostback)
    {
        this.Title = "Title nastavený ve fázi Init page, nepřežije Postback.";
    }
}

Ve fázi Init samotného Labelu ještě jeho TrackViewState() neproběhl, tedy se Text nezachová.

Ve fázi Init stránky už proběhl TrackViewState() controlů, ale stránky samotné ještě ne, tedy třeba Title se nezachová.

Makra pro ExpandAllRegions, CollapseAllRegions (#region)

Již delší dobu používám ve Visual Studiu dvě makra – ExpandAllRegions a CollapseAllRegions, ke kterým mám přidružené klávesové zkratky Ctrl+Alt+NumPlus a Ctrl+Alt+NumMinus. Makra jsou prostá, rozbalí všechny regiony v kódu, či zabalí všechny #regiony v kódu (což se od doby, kdy si VS2005+ pamatuje poslední sbalení/rozbalení u každého souboru, narozdíl od předchozích verzí, kde se vždy vše dalo otevírat sbalené, ukázalo jako neobyčejně potřebnná funkce).

Makra jsou prostá:

Sub ExpandAllRegions()
    DTE.ExecuteCommand(&quot;Edit.StopOutlining&quot;)
    DTE.ExecuteCommand(&quot;Edit.StartAutomaticOutlining&quot;)
End Sub

Sub CollapseAllRegions()
    ExpandAllRegions()
    Dim objSelection As TextSelection
    objSelection = DTE.ActiveDocument.Selection
    objSelection.StartOfDocument()
    While (objSelection.FindText(&quot;#region&quot;))
        objSelection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstColumn)
        DTE.ExecuteCommand(&quot;Edit.ToggleOutliningExpansion&quot;)
        objSelection.StartOfDocument()
    End While
    DTE.ActiveDocument.Selection.StartOfDocument()
End Sub

…a přiřazení klávesových zkratek je v Tools ~ Options ~ Environment ~ Keyboard ~ …a dále dohledat název makra dle procedury, takže např. Macros.MyMacros.Havit.CollapseAllRegions, atd.

Klientské (ne)cachování skriptů z WebResource.axd

Skripty emitované do stránky pomocí odkazů do assembly, pomocí WebResource.axd, se v browseru cachují pouze pokud máme debug-kompilaci vypnutou:

<compilation debug="false" />