October 11, 2018

ASP.NET Health Monitoring with Custom Events

In the last post we have created a custom provider to send log data to WCF service client using ASP.NET Health Monitoring feature. We have seen that there are multiple events which we can map to the provider in order to log information about that events. Just to recap here is the list of default events available by ASP.Net Health Monitoring feature.

  • All Events
  • Heartbeats
  • Application Lifetime Events
  • Request Processing Events
  • Infrastructure Errors
  • Request Processing Errors
  • All Audits
  • Failure Audits
  • Success Audits

These events could provide plenty of useful information which can help us to analyze application state if there comes any problem while running in production environment. But there may be the case where you might want to log your own custom event. For example, I want to log a hit event for specific page using this feature rather than use some other logging library or write my own, which further may require extra configuration steps or wrapper classes. Writing your own custom event helps you to log event information with ASP.Net Health Monitoring feature and saves you from that extra effort.

Lets start writing the custom event.

First we have to inherit base class WebRequestEvent found in the namespace System.Web.Management. While calling the base constructor we have to provide eventCode. Note that for custom events we have available event codes starting from 100000. You may find full list of event codes defined as constant fields in WebEventCodes sealed class. The Last EventCode constant defined is:

public const int WebExtendedBase = 100000;

For custom events we have to use codes starting from this number. In this exmaple I am using constant event code variable by adding to 10 to the WebExtendedBase event code value, as:

private const int EVENT_CODE = System.Web.Management.WebEventCodes.WebExtendedBase + 10;

10 is just an arbitrary value, you can pick any number.

If you want to add extra information then you have to write an override of Raise() function, which is not necessary, but in this example I am writing this override to add my custom message, in a class level private variable I am using this variable to add information about current UserId from session variable if present:

private string defaultLocalMessage = "";

Which I want to log alongwith other fields. But this variable could not be directly accessible from WebBaseEvent object's properties which we see in last example, in ProcessEvent() method of custom provider. If we can not directly access additional variables then what is the benefit of using this variable? Well, There is a workaround!

Any additional information holding by custom variables, can be used in another overriden function FormatCustomEventDetails. Although this function returns void, it will not directly return any value but this function is interally called when the provider invokes one of the ToString() methods. So, finally when you call ToString() function for WebBaseEvent object in the function ProcessEvent(), you will get that additional information that you holded in local variables and added to the WebEventFormatter object in another overriden method FormatCustomEventDetails(). When you see the code you will get it more clear.

Here is the complete code listing for custom event class PageHitRequestEvent.

public class PageHitRequestEvent : System.Web.Management.WebRequestEvent
{
    private string defaultLocalMessage = "";
    private const int EVENT_CODE = System.Web.Management.WebEventCodes.WebExtendedBase + 10;

    public PageHitRequestEvent(string eventMessage, object eventSource)
        :
        base(eventMessage, eventSource, EVENT_CODE)
    {
        
    }

    // Raises the PageHitRequestEvent.
    public override void Raise()
    {
        // prepare custom message.
        defaultLocalMessage = "";
        if(HttpContext.Current == null)
        {
            defaultLocalMessage = ", LocalMessage: {Request Context is null";
        }
        else
        {
            defaultLocalMessage += "SessionID: " + HttpContext.Current.Session.SessionID;

            string userId = HttpContext.Current.Session["UserId"] as string;
            if (string.IsNullOrEmpty(userId))
            {
                defaultLocalMessage += ", UserId: (Anonymous)";
            }
            else
            {
                defaultLocalMessage += ", UserId: " + HttpContext.Current.Session["UserId"];
            }
            defaultLocalMessage += "}";
        }
        
        // raise the event. 
        base.Raise();
    }

    public override void FormatCustomEventDetails(WebEventFormatter formatter)
    {
        base.FormatCustomEventDetails(formatter);

        // Add custom data.
        formatter.AppendLine("");

        formatter.IndentationLevel += 1;

        formatter.TabSize = 4;

        formatter.AppendLine("* PageHitRequestEvent Start *");

        // Display custom event information.
        formatter.AppendLine(defaultLocalMessage);
              
        formatter.AppendLine("* PageHitRequestEvent End *");

        formatter.IndentationLevel -= 1;
    }   
    
}

The second step is to raise this event from the source we want to log information about. Lets say we have some critical page in WebForms application or Contoller/Action in MVC Application, and we want to log data every time user hit that URL. Only we have to create the object of custom event class and simply call the Raise() method, which will just trigger the event and any related provider will handle this event as usual.

PageHitRequestEvent myEventObject = new PageHitRequestEvent("some logging message", this);
// raise the event.
myEventObject.Raise();

If you are using the custom provider from my previous post, where I logged detailed information from ToString() method, you will also get the information which you have written to the WebEventFormatter object in FormatCustomEventDetails method.

Finally comes the configuration part. Although in custom provider example, we have mapped eventName="All Events" in rules section, which will allow the corresponding provider to handle all events, so also our custom event we have created in this example.

But also if you want you can separately add custom event and its mapping with the desired provider.

First add the following line in eventMappings tag to define custom event name.

 <add name="My Event" type="PageHitRequestEvent" />

Second add following line in rules tag to map this event to the desired provider.

 <add name="My Event Rule" eventName="My Event" provider="FailedAuthenticationProvider2"
            minInstances="1" maxLimit="Infinite" minInterval="00:00:00" custom="" />
 

Now the given provider should be able to handle the custom event when triggered.

Resources:

October 4, 2018

ASP.NET Health Monitoring using Custom Provider

ASP.NET Health Monitoring is a useful tool for logging error information that could ease diagnosing problems in a deployed application.

I have found this great article at Microsoft for configuring health monitoring feature in web projects.
Logging Error Details with ASP.NET Health Monitoring (C#)

Although builtin providers are more than enough to provide a rich information about the state of application. But there could be a chance you need to create custom provider to log additional information or additional storage option. Like in my case we need to log data in separate database which our application do not have direct access, but we have to call WCF service which would store data in target database. For this requirement I have written my custom provider.

Before proceeding to write code for custom provider, I recommend you to review the above mentioned article for health monitoring which will give you a strong background information.

Lets start writing custom provider class.

public class WCFCustomWebEventProvider : WebEventProvider
{
 //WCF service's client object.
    MiddleWare.ServiceClient _client = new MiddleWare.ServiceClient();
 
    public override void Initialize(String name, System.Collections.Specialized.NameValueCollection config)
    {
        base.Initialize(name, config);
    }

    public override void Flush()
    {

    }

    public override void ProcessEvent(WebBaseEvent raisedEvent)
    {
  //prepare StringBuilder with required information from WebBaseEvent object
        StringBuilder sb = new StringBuilder();
        sb.Append("EventId: " + raisedEvent.EventID + ", ");
        sb.Append("EventTime: " + raisedEvent.EventTime + ", ");
        sb.Append("EventTimeUtc: " + raisedEvent.EventTimeUtc + ", ");
        sb.Append("EventType: " + raisedEvent.GetType().Name + ", ");
        sb.Append("EventSource: " + raisedEvent.EventSource + ", ");
        sb.Append("EventSequence: " + raisedEvent.EventSequence + ", ");
        sb.Append("EventOccurrence: " + raisedEvent.EventOccurrence + ", ");
        sb.Append("EventCode: " + raisedEvent.EventCode + ", ");
        sb.Append("EventDetailCode: " + raisedEvent.EventDetailCode + ", ");
        sb.Append("Message: " + raisedEvent.Message + ", ");
        sb.Append("ApplicationPath: " + WebBaseEvent.ApplicationInformation.ApplicationPath + ", ");
        sb.Append("ApplicationVirtualPath: " + WebBaseEvent.ApplicationInformation.ApplicationVirtualPath + ", ");
        sb.Append("MachineName: " + WebBaseEvent.ApplicationInformation.MachineName + ", ");
        sb.Append("RequestUrl: " + HttpContext.Current.Request.Url.AbsoluteUri + ", ");
        sb.Append("Details: " + raisedEvent.ToString());

  //pass the string to WCF client object.
        _client.CreateLog(sb.ToString());
    }

    public override void Shutdown()
    {

    }
}

I have tried to keep it simpler to meet basic requirements, and avoid additional changes which you may need for performance requirements like make use of buffering.

Here, in the ProcessEvent override method, I prepared a string variable build up the required information from argument WebBaseEvent raisedEvent. Then the final string message containing all required logging details is then passed to the WCF client. Putting all the logging information in a single string variable is obviously not a good approach, I did here only to keep things simple and try to focus on the logic to add custom provider. A better approach could be to split data of our interest in separate variables and update the WCF method to accept multiple parameters.

Here we are done with custom provider's C# code.

I assumed that you have read the above mentioned article, so now I will move to the web.config part.

Add the following healthMonitoring tag inside system.web.

<healthMonitoring enabled="true">
   <providers>
  <add name="WCFCustomWebEventProvider_1" type="WCFCustomWebEventProvider" />
   </providers>
   <rules>
   <add name="All Events Rule" eventName="All Events" provider="WCFCustomWebEventProvider_1"
    minInstances="1" maxLimit="Infinite" minInterval="00:00:00" custom="" />
   </rules>
   <eventMappings>
  <add name="All Events" type="System.Web.Management.WebBaseEvent,System.Web,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a"
   startEventCode="0" endEventCode="2147483647" />
   </eventMappings>
</healthMonitoring>

For simplicity, I am logging All Events, and also removed the configuration sections for bufferModes and profiles which you can add if you need more control over logging. Note that I have mentioned the custom provider WCFCustomWebEventProvider in the type attribute while adding provider.

At-minimum, health monitoring needs you to configure three basic elements.

  1. Providers: You have to specify a provider which will decide the storage target and do the work to store logging information. You can also define multiple providers like for some critical errors, you need to log information in the database and also want to generate an email to the system administrator. In our exmaple, we have used only one custom provider which will store information by calling WCF client method.

  2. EventMappings: It contains all the events names we are interested to log information about. In our example I placed a single event name All Events, which enables the application to log data about all events. If you dont need to log data about all events, you can define the desired events according to the requirements.

    Some other available events are:

    • All Events
    • Heartbeats
    • Application Lifetime Events
    • Request Processing Events
    • Infrastructure Errors
    • Request Processing Errors
    • All Audits
    • Failure Audits
    • Success Audits
  3. Rules: Rule is basically a mapping you have to define, to decide which event should mapped to which provider. In our example, I have defined that eventName="All Events" should be handled by provider="WCFCustomWebEventProvider_1".