Tuesday, March 19, 2013

Load Performance Counters from XML file.

I have been playing recently with the Azure Autoscaling Block and Diagnostics (primarily using this article as a reference). One of the issues is creating performance counters. It seemed like it would be frustrating to add similar but ever so slightly different PowerShell scripts to every single role. It seemed it would be a bit easier to write a single script, and have the script add counters based on an XML file. So I wrote a little script I thought I'd share that will install Performance Counters based on a simple XML file. It will also automatically generate the Base objects you need to go along with certain types.

Here is the PowerShell script:
Param(
  [string]$counterFile
)

Function ParseCounterXMLFile
{

    [xml]$counters = Get-Content  $counterFile
    foreach ($category in $counters.counters.category)
    {
        $categoryName = $category.Name;

        # Delete category if it already exists.
        $exists = [System.Diagnostics.PerformanceCounterCategory]::Exists($categoryName)
        if ($exists)
        {
        [System.Diagnostics.PerformanceCounterCategory]::Delete($categoryName)
        }
 
        $counterData = new-object System.Diagnostics.CounterCreationDataCollection

        # For each counter in the category, add it to the new category collection.
        foreach ($counterElement in $category.counter)
        {
            $name = $counterElement.Attributes.ItemOf("name").Value
            $type = $counterElement.Attributes.ItemOf("type").Value
            AddCounter $counterData $name $type

            $baseType = ""

            switch ($type)
            {
                "AverageTimer32" { $baseType = "AverageBase" }
                "AverageCount64" { $baseType = "AverageBase" }
                "CounterMultiTimer" { $baseType = "CounterMultiBase" }
                "CounterMultiTimerInverse" { $baseType = "CounterMultiBase" }
                "CounterMultiTimer100Ns" { $baseType = "CounterMultiBase" }
                "CounterMultiTimer100NsInverse" { $baseType = "CounterMultiBase" }
                "RawFraction" { $baseType = "RawBase" }
                "SampleFraction" { $baseType = "SampleBase" }
                default { }
            }

            if ($baseType)
            {
                AddCounter $counterData "$name Base" $baseType
            }
        }

        # Create the counters in this category.
        [System.Diagnostics.PerformanceCounterCategory]::Create($categoryName, $categoryName, [System.Diagnostics.PerformanceCounterCategoryType]::SingleInstance, $counterData)
    }
}

Function AddCounter($counterData, $name, $type)
{
    $counter = new-object  System.Diagnostics.CounterCreationData
    $counter.CounterType = [System.Diagnostics.PerformanceCounterType] $type;
    $counter.CounterName = $name

    $counterData.Add($counter)

    write $name $type
}

ParseCounterXMLFile

And here is an example XML file:
<counters>
    <category name="My Test Performance Counters">
        <counter type="AverageTimer32" name="Avg Timer Test" />
        <counter type="NumberOfItems32" name="Counter Test 1" />
    </category>
    <category name="My Test Performance Counters Cat 2">
        <counter type="NumberOfItems32" name="Counter Test 2" />
    </category>
</counters>

You can use this in Azure by adding a StartupTask that calls a .cmd file that issues this command:
powershell -ExecutionPolicy Unrestricted -File .\loadperformancecounters.ps1 .\PerfCounters.xml

Update: This was working in the Compute Emulator, but failed on Azure. Upon troubleshooting I discovered something very strange... Using the bracket operator on an XmlAttributeCollection does not work in whatever version of .NET or Powershell being used in Azure instances. In the end, all I had to do was replace two lines of code:

            $name = $counterElement.Attributes["name"].Value
            $type = $counterElement.Attributes["type"].Value

was changed to:

            $name = $counterElement.Attributes.ItemOf("name").Value
            $type = $counterElement.Attributes.ItemOf("type").Value

This change is reflected in the code up above. I have absolutely no idea why this would make a difference