Public/Get-tpcPerformanceCounterInfo.ps1


function Get-tpcPerformanceCounterInfo {
<#
    .SYNOPSIS
 
    .DESCRIPTION
        Resolves performance counter IDs from names or validates existing composite IDs. Supports local and remote lookup.
 
    .PARAMETER SearchTerm
        Composite ID ("238-6") or localized counter name/pattern (wildcards supported).
 
    .PARAMETER ComputerName
        Target remote machine for remote lookup.
 
    .PARAMETER Credential
        Alternate credentials for remote execution.
 
    .EXAMPLE
        Get-tpcPerformanceCounterInfo -SearchTerm "238-6"
 
        Validates and resolves a composite counter ID.
 
    .EXAMPLE
        Get-tpcPerformanceCounterInfo -SearchTerm "Processor"
 
        Searches for counters matching the name pattern on the local machine.
 
    .EXAMPLE
        Get-tpcPerformanceCounterInfo -SearchTerm "Processor" -ComputerName 'lab-node1'
 
        Searches for counters on a remote machine.
    #>


    [CmdletBinding(DefaultParameterSetName = 'Local')]
    param(
        [Parameter(ParameterSetName = 'Remote', Mandatory)]
        [string] $ComputerName,

        [Parameter(ParameterSetName = 'Remote')]
        [pscredential] $Credential,

        [Parameter(ParameterSetName = 'Remote', Mandatory)]
        [Parameter(ParameterSetName = 'Local',  Mandatory)]
        [string] $SearchTerm
    )

    $Result  = [System.Collections.Generic.List[object]]::new()
    $param   = @{}
    $isLocal = $PSCmdlet.ParameterSetName -eq 'Local'

    if ( -not $isLocal ) {

        $param.Computername = $ComputerName

        if ( $null -ne $Credential ) {
            $param.Credential = $Credential
        }
    }

    try {

        if ( $PSCmdlet.ParameterSetName -eq 'Remote' -and -not $(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet) ) {
            THROW "Remote computer $ComputerName not reachable. Aborting"
        }

        # is ID
        if ( $SearchTerm -match '^\d+-\d+$' ) {

            $IdParts  = $SearchTerm -split '-'
            $SetId    = [int]$IdParts[0]
            $PathId   = [int]$IdParts[1]

            try {

                # Lookup IDs -> Names
                $SetName  = Get-PerformanceCounterLookup -ID $SetId  @param -ErrorAction SilentlyContinue
                $PathName = Get-PerformanceCounterLookup -ID $PathId @param -ErrorAction SilentlyContinue

                if ( $SetName -and $PathName ) {

                    if ( $isLocal ) {
                        $ListSetObj = Get-Counter -ListSet $setname -ErrorAction SilentlyContinue
                    } else {
                        $scriptblock = { param([string]$arg1) Get-Counter -ListSet $arg1 -ErrorAction SilentlyContinue }
                        $ListSetObj  = Invoke-Command -ScriptBlock $scriptblock @param -ArgumentList $setname
                    }

                    if (-not $ListSetObj) { throw "CounterSet '$SetName' found by ID but not accessible via Get-Counter." }

                    $SetType = $ListSetObj.CounterSetType.ToString()
                    $counterInstancesValues = @("-")

                    # Handle MultiInstance counters
                    if ( $SetType -eq 'MultiInstance' ) {
                        $counterInstances = $ListSetObj.PathsWithInstances
                        # Regex to extract instance name between parenthesis
                        $searchPattern = '\(([^)]+)\)\\'

                        $counterInstancesValues = $counterInstances | ForEach-Object {
                                if ( $_ -match $searchPattern ) { $matches[1] }
                        } | Select-Object -Unique
                    }

                    $Result.Add([PSCustomObject]@{
                        ID          = $SearchTerm
                        CounterSet  = $SetName
                        Path        = $PathName
                        SetType     = $SetType
                        Instances   = $($counterInstancesValues -join ', ')
                    })

                } else {
                    Write-Host "Could not resolve ID '$SearchTerm' to counter names." -ForegroundColor Red
                    if (-not $SetName)  { Write-Host " Set ID $SetId not found" -ForegroundColor Yellow }
                    if (-not $PathName) { Write-Host " Path ID $PathId not found" -ForegroundColor Yellow }
                }

            } catch {
                Write-Error "Error resolving ID '$SearchTerm': $($_.Exception.Message)"
            }

        # Input is a Name (Search Term)
        } else {

            # Retrieve ALL sets
            if ( $isLocal ) {
                $AllSets = Get-Counter -ListSet * -ErrorAction SilentlyContinue
            } else {
                $scriptblock = { Get-Counter -ListSet * -ErrorAction SilentlyContinue }
                $AllSets     = Invoke-Command -ScriptBlock $scriptblock @param
            }

            foreach ( $CounterSet in $AllSets ) {

                $MatchingPaths = $CounterSet.Paths | Where-Object {
                    ($_ -like "*$SearchTerm*") -or ($CounterSet.CounterSetName -like "*$SearchTerm*")
                }

                foreach ( $CounterPath in $MatchingPaths ) {

                    try {
                        # 1. Get SET ID
                        $SetId = Get-PerformanceCounterLookup -Name $CounterSet.CounterSetName @param -ErrorAction SilentlyContinue

                        # 2. Clean PATH NAME
                        $RawName = ($CounterPath -split '\\')[-1]

                        # Only remove '(*)' if strictly necessary (wildcard placeholder)
                        $PathName = $RawName -replace '\(\*\)', ''

                        # 3. Get PATH ID
                        $PathId = Get-PerformanceCounterLookup -Name $PathName @param -ErrorAction SilentlyContinue | Select-Object -First 1

                        # 4. Build Composite ID
                        $CompositeId = if ( $SetId -and $PathId ) { "$SetId-$PathId" } else { "N/A" }

                        # Gather Metadata
                        $SetType = $CounterSet.CounterSetType.ToString()
                        $counterInstancesValues = @("-")

                        if ( $SetType -eq 'MultiInstance' ) {

                            $counterInstances = $CounterSet.PathsWithInstances
                            # Regex: Matches content inside parenthesis before a backslash
                            $searchPattern = '\(([^)]+)\)\\'

                            $counterInstancesValues = $counterInstances | ForEach-Object {
                                if ( $_ -match $searchPattern ) { $matches[1] }
                            } | Select-Object -Unique

                        } elseif ( $SetType -ne 'SingleInstance' ) {

                            $SetType = "Unknown ($SetType)"

                        }

                        $Result.Add([PSCustomObject]@{
                            ID          = $CompositeId
                            CounterSet  = $CounterSet.CounterSetName
                            Path        = $CounterPath
                            SetType     = $SetType
                            Instances   = $($counterInstancesValues -join ', ')
                        })

                    } catch {
                        # Warning instead of Error to avoid stopping the entire loop for one bad counter
                        Write-Warning "Skipping path '$CounterPath': $($_.Exception.Message)"
                    }
                }
            }
        }

        if ( $Result.Count -gt 0)  {
            $Result | Sort-Object -Property CounterSet, Path | Format-Table -AutoSize -Wrap
        } else {
            Write-Host "No matching performance counters found for search term: $SearchTerm" -ForegroundColor Yellow
        }

    } catch {
        Write-Error "Critical error in Get-tpcPerformanceCounterInfo: $($_.Exception.Message)"
    }

}