Microsoft PowerShell allows us to create, read and write performance counters via .NET with ease (check PerformanceCounterCategory in System.Diagnostics). However, there are some gotchas when trying it the first time …Beginning with .NET Framework 1.1 Performance Counters are exposed via the [Systems.Diagnostics] namespace. How to create custom performance counters has been described multiple times all over different places before so I will spare that out. As a quick summary (taken from the biz.dfch.PS.System.Logging module):
$categoryName = "biz.dfch.PS.System.Logging"$PerfCounter = [ordered] @{ "Out-MessageDebug AverageCount64" = [System.Diagnostics.PerformanceCounterType]::AverageCount64; "Out-MessageDebug AverageBase" = [System.Diagnostics.PerformanceCounterType]::AverageBase; "Out-MessageInfo AverageCount64" = [System.Diagnostics.PerformanceCounterType]::AverageCount64; "Out-MessageInfo AverageBase" = [System.Diagnostics.PerformanceCounterType]::AverageBase; "Out-MessageDebug Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageDebug Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageInfo Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageInfo Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageNotice Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageNotice Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageWarning Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageWarning Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageError Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageError Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageAlert Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageAlert Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageCritical Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageCritical Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; "Out-MessageEmergency Calls Total" = [System.Diagnostics.PerformanceCounterType]::NumberOfItems32; "Out-MessageCritical Calls/sec" = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32; }; [Boolean] $fReturn = $false; $fReturn = [System.Diagnostics.PerformanceCounterCategory]::Exists($categoryName); if ($fReturn) { [System.Diagnostics.PerformanceCounterCategory]::Delete($categoryName) }; Remove-Variable counterData; $counterData = new-object System.Diagnostics.CounterCreationDataCollection; $PerfCounter.Keys | % { Write-Host $_; Write-Host $PerfCounter.Item($_); $counter = New-Object System.Diagnostics.CounterCreationData; $counter.CounterType = $PerfCounter.Item($_); $counter.CounterName = $_; $counterData.Add($counter); } # foreach # requires administrative permissions [System.Diagnostics.PerformanceCounterCategory]::Create($categoryName, $categoryName, [System.Diagnostics.PerformanceCounterCategoryType]::SingleInstance, $counterData);
Setting the counter for testing can be done with something like this:
$aCounter = @(); $PerfCounter.Keys | % { Write-Host $_; Write-Host $PerfCounter.Item($_); $counter = New-Object System.Diagnostics.PerformanceCounter -ArgumentList $categoryName, $_, $false; $aCounter += $counter; $counter; } # foreach 1..1000 | % { $value = Get-Random -Minimum 1 -Maximum 100; $factor1 = (Get-Random -Minimum 0 -Maximum 10)/10; $factor2 = (Get-Random -Minimum 0 -Maximum 10)/10; [void] $aCounter[0].IncrementBy($factor2 * $value); [void] $aCounter[1].Increment(); [void] $aCounter[2].IncrementBy($factor1 * $value); [void] $aCounter[3].Increment(); [void] $aCounter[4].Increment(); [void] $aCounter[5].IncrementBy($value/100); $null = Start-Sleep -Milliseconds 10; } # foreach foreach ($c in $aCounter) { $c; $c.Close(); $c.Dispose(); } # foreach
The problem you might encounter lies in the fact that we might play around with the counters and start re-creating the the performance counters along with the category (see red lines of code above). Doing this essential re-creates a new category which is fine. However, internally the memory section related to the category are not re-initialised but re-used. So when trying with another set of counters we might not get any or wrong results, as in internally the “old” (already deleted) counter is used. This is especially true when you change the type of counters. It is actually documented on Microsoft Connect as Setting PerformanceCounter.RawValue on one counter corrupts the value of another counter in response to Restore PerformanceCounter values after recreating category. However, within the MSDN documentation I could not really find anything about this constraint (nor in any blog entries). So it actually cost me 3h of my life… Hopefully you get it faster!