A Warm-Up Solution

To prepare you for the upcoming battle you were offered a warm-up challenge. The basic challenge was to take the output from Get-Counter and pipe it through a command you created to output more user-friendly output. As a bonus you were also challenged  to display the default results as a table. As with almost everything in the PowerShell world there are options and alternatives.Today the Chairman shares one possible solution and some of the reasoning behind it. This solution is not necessarily better than yours but rather should be something you can learn from.

Requirements

Because this is going to be a stand-alone function it has to live in .ps1 file. You might have to consider who might be using this script. What operating system or version of PowerShell will they be running? The suggested solution requires that the user be running PowerShell 5.1 or later, even though technically you could get by with earlier versions. The script is also requiring the Microsoft.PowerShell.Diagnostics module.

#requires -version 5.1
#requires -module Microsoft.PowerShell.Diagnostics

This is the module that contains the Get-Counter cmdlet which you need in order for you command to work. Requiring it also has the added benefit of loading it into your PowerShell session if it is not running. This is important for parameters definitions.

Parameters

The only parameter the sample solution requires is one for the performance counter samples. This should be mandatory and come in from the pipeline.

param (
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet]$CounterSample
    )

It is often useful to indicate the object type which we’ve done here. Note that without the requirement for the diagnostics module, PowerShell throws an error when loading this function because the type has not been loaded yet. But using the requires statement imports the module if not already loaded which adds the necessary type.

Naming

We were able to see a few people’s efforts on this challenge. One major variance was in naming the function. There is no reason not use a verb from the list created by Get-Verb. There are plenty of choices that should work such as Optimize, Format, or Convert. If you wanted to use a non-standard verb, you could create it as an alias.

function Convert-Counter {
    [CmdletBinding()]
    [alias("cc","Reformat-Counter")]

There is nothing wrong with providing an alias that someone can use at the command prompt to run your code.

Creating Custom Objects

The core of this challenge was restructuring the output from Get-Counter into something easier to read and consume. You would piped Get-Counter to Get-Member to discover property names. In the work we saw many of you figured out that you needed to process the CounterSamples property. Parsing the values is where you can get creative. You could use regular expression patterns to get the computername and other values. In the sample solution we took the easy way and split the Path property into an array.

foreach ($sample in $CounterSample.countersamples) {
            Write-Verbose "Parsing $($sample.path)"
            #turn the string into an array, filtering out blanks
            $arr = $sample.path -split "\\" | Where-Object {$_}

One thing we noticed using this technique was an extra blank element. Piping the array to Where-Object is telling PowerShell to only keep objects where something exists. If there is a value then $_ will implicitly be True.

Because the array is consistent, it is easy to generate a custom object.

[PSCustomObject]@{
    PSTypename   = "myCounter"
    Timestamp    = $sample.Timestamp
    Computername = $arr[0].ToUpper()
    Counterset   = $arr[1]
    Counter      = $arr[2]
    Value        = $sample.cookedValue
}

We’ll come back to the use of PSTypename in a bit.

The Result

Here is our sample solution in action.

Converting Get-Counter samples

The function can consume counter information and write objects to the pipeline.

get-counter -ListSet processor -ComputerName SRV1,SRV2 | get-counter | convert-counter | out-gridview -Title "Processor Stats"

Consuming Converted Counters

Formatting

The formatting challenge was to take the default output which is a list and present it as a table. In other words, the output of your command should display results in a table, not a list. In order to achieve this you need to create a format.ps1xml file using the object’s typename. In the sample solution we are adding a PSTypename property.

Viewing the custom type name

You might also have used a PowerShell class or created a custom object and inserted a new typename.

$obj.psobject.typenames.insert(0,"mycounter")

Regardless of technique you now need to create a custom xml file. Often the best thing to do is fine an object type in an existing file $pshome\DotNetTypes.format.ps1xml that is close the output you want, copy and paste it into a new file and modify to fit your needs. This is admittedly a tedious process. Another option is to use the New-PSFormatXML command that is part of the PSScriptTools module which you can install from the PowerShell gallery. This functions will create a format.ps1xml file based on an object.

Get-counter | convert-counter | select -first 1 | new-psformatxml -Path c:\work\mycounter.format.ps1xml

You only need to give it a single instance of the object. Here is what the result can look like.

<?xml version="1.0" encoding="UTF-8"?>
<!--
format type data generated 02/18/2019 09:50:41
by BOVINE320\Jeff
-->
<Configuration>
  <ViewDefinitions>
    <View>
      <!--Created 02/18/2019 09:50:41 by BOVINE320\Jeff-->
      <Name>default</Name>
      <ViewSelectedBy>
        <TypeName>Selected.System.Management.Automation.PSCustomObject</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <!--Delete the AutoSize node if you want to use the defined widths.-->
        <AutoSize />
        <TableHeaders>
          <TableColumnHeader>
            <Label>Timestamp</Label>
            <Width>23</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Computername</Label>
            <Width>15</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Counterset</Label>
            <Width>62</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Counter</Label>
            <Width>18</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Value</Label>
            <Width>19</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <!--
            By default the entries use property names, but you can replace them with scriptblocks.
            <Scriptblock>$_.foo /1mb -as [int]</Scriptblock>
-->
              <TableColumnItem>
                <PropertyName>Timestamp</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Computername</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Counterset</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Counter</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Value</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

You can modify the file as you need to changing column headings or creating custom values. Here is the final format.ps1xml file we came up with.

<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
    <ViewDefinitions>
        <View>
            <Name>default</Name>
            <ViewSelectedBy>
                <TypeName>myCounter</TypeName>
            </ViewSelectedBy>
            <GroupBy>
                <ScriptBlock>"$($_.computername) [$($_.timestamp)]"</ScriptBlock>
                <Label>Instance</Label>
            </GroupBy>
            <TableControl>
                <AutoSize/>
                <TableHeaders>
                    <TableColumnHeader>
                        <Label>Counterset</Label>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>Counter</Label>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>Value</Label>
                        <Alignment>left</Alignment>
                    </TableColumnHeader>
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                            <TableColumnItem>
                                <PropertyName>Counterset</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Counter</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Value</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                </TableRowEntries>
            </TableControl>
        </View>
        <View>
            <Name>timestamp</Name>
            <ViewSelectedBy>
                <TypeName>myCounter</TypeName>
            </ViewSelectedBy>
            <GroupBy>
                <PropertyName>Timestamp</PropertyName>
                <Label>Time</Label>
            </GroupBy>
            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Width>15</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>50</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>20</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>16</Width>
                        <Alignment>left</Alignment>
                    </TableColumnHeader>
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <Wrap/>
                        <TableColumnItems>
                            <TableColumnItem>
                                <PropertyName>Computername</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Counterset</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Counter</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Value</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                </TableRowEntries>
            </TableControl>
        </View>
    </ViewDefinitions>
</Configuration>

Our solution creates a default view grouped by a custom scriptblock that shows the computername and timestamp. It also creates a second view called ‘TimeStamp’. This comes in handy when processing counters over a period of time. In order to use this file, it has to be imported into PowerShell. We put the file in the same folder as the ps1 file and add this command:

Update-Formatdata -appendpath $psscriptroot\myCounter.format.ps1xml

One thing to point out if you are new to working with format files is that because there is often a bit of trial and error you may need to start a new PowerShell session to load each revision. Here is the new result.

Formatted Output

When processing multiple computers you need to sort on the Computername property.

Processing multiple computers

The other custom view we defined is useful when monitoring samples over a period of time.

Formatting results over time

The Final Result

Here is the complete script file of our sample solution.

#requires -version 5.1
#requires -module Microsoft.PowerShell.Diagnostics
function Convert-Counter {
    [CmdletBinding()]
    [alias("cc","Reformat-Counter")]
    param (
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet]$CounterSample
    )
    begin {
        Write-Verbose "Starting $($myinvocation.MyCommand)"
    }
    process {
        Write-Verbose "Processing $($countersample.CounterSamples.count) performance counter samples"
        foreach ($sample in $CounterSample.countersamples) {
            Write-Verbose "Parsing $($sample.path)"
            #turn the string into an array, filtering out blanks
            $arr = $sample.path -split "\\" | Where-Object {$_}
            Write-Verbose "Using value $($sample.cookedValue)"
            [PSCustomObject]@{
                PSTypename   = "myCounter"
                Timestamp    = $sample.Timestamp
                Computername = $arr[0].ToUpper()
                Counterset   = $arr[1]
                Counter      = $arr[2]
                Value        = $sample.cookedValue
            }
        } #foreach
    }
    end {
        Write-Verbose "Ending $($myinvocation.mycommand)"
    }
}
Update-Formatdata -appendpath $psscriptroot\myCounter.format.ps1xml

Remember, these warm-up exercises and preludes aren’t really a competition. They are designed to get you ready for the Iron Scripter event at the PowerShell+DevOps Global Summit and hopefully teach you something new along the way.

Stay tuned for the next prelude exercise.