Public/Get-tpcAvailableCounterConfig.ps1

     function Get-tpcAvailableCounterConfig {
     <#
     .SYNOPSIS
          Retrieves, validates, and displays detailed information about available performance counter configurations from all configured paths.
 
     .DESCRIPTION
          This function scans all configured paths (using Get-tpcConfigPaths) for JSON configuration files with the 'tpc_' prefix
          and provides comprehensive information about each configuration including counter details, JSON schema validation status,
          and optional counter availability testing.
 
          Results are grouped by path similar to Get-Module -ListAvailable. Duplicate configurations across paths are automatically
          detected and marked to help identify potential conflicts. The function validates each configuration against the module's
          JSON schema and optionally tests counter availability on the system.
 
          Template files (containing 'template' in the basename) are automatically excluded from the results.
 
     .PARAMETER Raw
          If specified, returns raw PSCustomObject array instead of formatted console output.
          Useful for further processing or filtering of configuration data.
 
     .PARAMETER TestCounters
          If specified, tests each counter for availability on the current system.
          This validates that counters can actually be queried but may be slow with many counters.
 
     .EXAMPLE
          Get-tpcAvailableCounterConfig
 
          Shows formatted overview of all available configurations from all configured paths.
          Displays configuration names, descriptions, counter counts, and validation status.
 
     .EXAMPLE
          Get-tpcAvailableCounterConfig -TestCounters
 
          Shows formatted overview with counter availability testing from all configured paths.
          Validates each counter to ensure it's available on the current system.
 
     .EXAMPLE
          Get-tpcAvailableCounterConfig -Raw
 
          Returns raw configuration objects for further processing or custom filtering.
 
     .EXAMPLE
          Get-tpcAvailableCounterConfig | Where-Object { $_.JsonValid -eq $false }
 
          Lists only configurations with JSON validation errors (when used with -Raw).
 
     .OUTPUTS
          Formatted console output by default (grouped by path), PSCustomObject[] when -Raw is used.
 
     .NOTES
          This function requires the GripDevJsonSchemaValidator module for JSON schema validation.
          If not installed, validation will be skipped with a warning.
 
          Related commands:
          - Get-tpcConfigPaths: List all configured configuration paths
          - Start-tpcMonitor: Start monitoring with a specific configuration
          - Get-tpcPerformanceCounterInfo: Get counter IDs for creating custom configurations
     #>


     [CmdletBinding()]
     [OutputType([PSCustomObject[]])]
     param(
          [switch]    $Raw,
          [switch]    $TestCounters
     )

     try {

          # required module for JSON schema validation
          $skipSchemaValidation = $false

          if ( -not (Get-Module -Name GripDevJsonSchemaValidator -ListAvailable) ) {
               Write-Warning "Module 'GripDevJsonSchemaValidator' not found. Please install it with: Install-Module -Name GripDevJsonSchemaValidator"
               Write-Warning "JSON schema validation will be skipped."
               $skipSchemaValidation = $true
          }

          # Check if central schema file exists
          if ( -not (Test-Path $script:JSON_SCHEMA_FILE) ) {
               Write-Warning "Central schema file not found at: $script:JSON_SCHEMA_FILE. Skipping schema validation."
               $skipSchemaValidation = $true
          }

          # Get all configured paths using Get-tpcConfigPaths
          $ConfigPaths = Get-tpcConfigPaths

          if ( $ConfigPaths.Count -eq 0 ) {
               Write-Warning "No configuration paths found. Use Add-tpcConfigPath to add configuration directories."
               return @()
          }

          $AllResults         = @()
          $ConfigNamesFound   = @{}

          foreach ( $ConfigPath in $ConfigPaths ) {

               if ( -not (Test-Path $ConfigPath) ) {
                    Write-Warning "Configuration directory not found: $ConfigPath"
                    continue
               }

               $ConfigFiles = Get-ChildItem -Path $ConfigPath -Filter "tpc_*.json" -File | Where-Object { $_.BaseName -notlike "*template*" }

               if ( $ConfigFiles.Count -eq 0 ) {
                    Write-Verbose "No configuration files found with 'tpc_' prefix in: $ConfigPath"
                    continue
               }

               $PathResults = @()

               foreach ( $ConfigFile in $ConfigFiles ) {

               try {

                    $ConfigName    = $ConfigFile.BaseName -replace '^tpc_', ''

                    # Track duplicate configurations, 1st action for all files

                    $ConfigNameLower = $ConfigName.ToLower()
                    if ( $ConfigNamesFound.ContainsKey($ConfigNameLower) ) {
                         $ConfigNamesFound[$ConfigNameLower] += 1
                    } else {
                         $ConfigNamesFound[$ConfigNameLower] = 1
                    }

                    $JsonContent   = Get-Content -Path $ConfigFile.FullName -Raw -ErrorAction Stop

                    # Check for empty file
                    $isEmpty = [string]::IsNullOrWhiteSpace($JsonContent)

                    if ( -not $isEmpty ) {
                         $JsonConfig    = $JsonContent | ConvertFrom-Json -ErrorAction Stop
                    }

                    # Determine if this is a duplicate
                    $IsDuplicate = $ConfigNamesFound[$ConfigNameLower] -gt 1

                    $SchemaValidation = @{IsValid = $true; Errors = @()}

                    if ( -not $isEmpty -and -not $skipSchemaValidation ) {

                         try {

                              $ValidationResult = Test-JsonSchema -SchemaPath $script:JSON_SCHEMA_FILE -JsonPath $ConfigFile.FullName -ErrorAction Stop 6>$null
                              $SchemaValidation.IsValid = $ValidationResult.Valid
                              if ( -not $ValidationResult.Valid ) {
                                   $SchemaValidation.Errors = @($ValidationResult.Errors | ForEach-Object { "$($_.Message) | Path: $($_.Path) | Line: $($_.LineNumber)" })
                              }

                         } catch {

                              $SchemaValidation.IsValid     = $false
                              $SchemaValidation.Errors      = @("Schema validation failed: $($_.Exception.Message)")
                         }

                    } else {

                         Write-Verbose "Skipping schema validation for $ConfigName in path $ConfigPath due to missing module or schema file."

                    }

                    $CounterDetails = @()

                    if ( -not $isEmpty ) {
                         foreach ( $CounterConfig in $JsonConfig.counters ) {

                         try {

                              $Counter = [CounterConfiguration]::new(
                                   $CounterConfig.counterID,
                                   $CounterConfig.counterSetType,
                                   $CounterConfig.counterInstance,
                                   $CounterConfig.title,
                                   $CounterConfig.type,
                                   $CounterConfig.format,
                                   $CounterConfig.unit,
                                   $CounterConfig.conversionFactor,
                                   $CounterConfig.conversionExponent,
                                   $CounterConfig.colorMap,
                                   $CounterConfig.graphConfiguration,
                                   $false,
                                   "",
                                   $NULL
                              )

                              $IsAvailable = if ($TestCounters) { $Counter.TestAvailability() } else { $null }

                              $CounterDetail = [PSCustomObject]@{
                                   Title          = $Counter.Title
                                   CounterId      = $Counter.counterID
                                   CounterPath    = $Counter.CounterPath
                                   Unit           = $Counter.Unit
                                   Format         = $Counter.Format
                                   Valid          = $IsAvailable
                                   ErrorMessage   = if ($TestCounters -and -not $IsAvailable) { $Counter.LastError } else { $null }
                                   InstancePath   = $Counter.CounterPath
                              }

                              $CounterDetails += $CounterDetail

                         } catch {

                              $CounterDetail = [PSCustomObject]@{
                                   Title          = $CounterConfig.title
                                   CounterId      = $CounterConfig.counterID
                                   CounterPath    = "Invalid"
                                   Unit           = $CounterConfig.unit
                                   Format         = $CounterConfig.format
                                   Valid          = $false
                                   ErrorMessage   = $_.Exception.Message
                                   InstancePath   = $($_.Exception.Message)
                              }

                              $CounterDetails += $CounterDetail
                         }
                    }
               }

                    $ConfigOverview = [PSCustomObject]@{
                         ConfigName               = $ConfigName
                         ConfigPath               = $ConfigPath
                         Description              = if ($isEmpty) { "Error: Empty configuration file" } else { $JsonConfig.description }
                         ConfigFile               = $ConfigFile.FullName
                         JsonValid                = if ($isEmpty) { $false } else { $SchemaValidation.IsValid }
                         JsonValidationErrors     = if ($isEmpty) { @("Configuration file is empty or contains only whitespace") } else { $SchemaValidation.Errors }
                         CounterCount             = $CounterDetails.Count
                         ValidCounters            = if ($TestCounters) { ($CounterDetails | Where-Object { $_.Valid -eq $true }).Count }  else { "Not tested" }
                         InvalidCounters          = if ($TestCounters) { ($CounterDetails | Where-Object { $_.Valid -eq $false }).Count } else { "Not tested" }
                         Counters                 = $CounterDetails
                         IsDuplicate              = $IsDuplicate
                    }

                    $PathResults += $ConfigOverview

               } catch {

                    Write-Error "Error processing configuration file '$($ConfigFile.Name)': $($_.Exception.Message)"

                    # Determine if this is a duplicate (also for error configs)
                    $ConfigNameLower = ($ConfigFile.BaseName -replace '^tpc_', '').ToLower()
                    $IsDuplicate = $ConfigNamesFound[$ConfigNameLower] -gt 1

                    $ErrorConfig = [PSCustomObject]@{
                         ConfigName               = $ConfigFile.BaseName -replace '^tpc_', ''
                         ConfigPath               = $ConfigPath
                         Description              = "Error loading configuration"
                         ConfigFile               = $ConfigFile.FullName
                         JsonValid                = $false
                         JsonValidationErrors     = @($_.Exception.Message)
                         CounterCount             = 0
                         ValidCounters            = if ($TestCounters) { 0 } else { "Not tested" }
                         InvalidCounters          = if ($TestCounters) { 0 } else { "Not tested" }
                         Counters                 = @()
                         IsDuplicate              = $IsDuplicate
                    }

                    $PathResults += $ErrorConfig

               }
          }

          # Add path results to overall results
          if ( $PathResults.Count -gt 0 ) {
               $AllResults += $PathResults
          }
     }

     # Identify duplicates for summary
     $duplicateNames = $ConfigNamesFound.Keys | Where-Object { $ConfigNamesFound[$_] -gt 1 }

          if ( $Raw ) {

               return $AllResults

          } else {

               # Group by path
               $groupedResults = $AllResults | Group-Object -Property ConfigPath

               foreach ( $pathGroup in $groupedResults ) {
                    Write-Host "`nConfiguration Path: $($pathGroup.Name)" -ForegroundColor Cyan
                    $separatorLine = "=" * $( $pathGroup.Name.Length + 22 )
                    Write-Host $separatorLine -ForegroundColor Cyan

                    foreach ( $result in $pathGroup.Group ) {

                         $configDisplay = $result.ConfigName
                         if ( $result.IsDuplicate ) {
                              $configDisplay += " [DUPLICATE]"
                         }
                         $configDisplay += " ($([System.IO.Path]::GetFileName($result.ConfigFile)))"

                         $separatorLine2 = "." * $( $pathGroup.Name.Length + 22 )
                         Write-Host ""
                         Write-Host $separatorLine2 -ForegroundColor Gray
                         Write-Host "`n$configDisplay" -ForegroundColor $(if ($result.IsDuplicate) { "Yellow" } else { "Green" })
                         Write-Host "Description: $($result.Description)" -ForegroundColor Gray

                         $JsonStatus    = if ( $result.JsonValid )    { "Valid JSON Schema" } else { "Invalid JSON Schema" }
                         $CounterStatus = if ( $TestCounters )        { "Counters: $($result.ValidCounters)/$($result.CounterCount)" } else { "Counters: $($result.CounterCount) (not tested)" }

                         if ( $result.JsonValid ) {
                              Write-Host "$JsonStatus, $CounterStatus" -ForegroundColor Green
                         } else {
                              Write-Host "$JsonStatus, $CounterStatus" -ForegroundColor Red
                              Write-Host "Schema Validation Errors:" -ForegroundColor Yellow
                              ForEach ( $errorMessage in $result.JsonValidationErrors ) {
                                   Write-Host " - $errorMessage" -ForegroundColor Red
                              }
                         }

                         if ( $result.IsDuplicate ) {
                              Write-Warning "This configuration exists in multiple paths. Consider removing duplicates."
                         }

                         if ( $result.Counters.Count -gt 0)  {
                              Write-Host "Counters:" -ForegroundColor Gray
                              if ( $TestCounters ) {
                                   $result.Counters | Format-Table Title, CounterId, Unit, Format, Valid, InstancePath -AutoSize -Wrap
                              } else {
                                   $result.Counters | Format-Table Title, CounterId, Unit, Format, InstancePath -AutoSize -Wrap
                              }

                         } else {

                              Write-Host "No counters found" -ForegroundColor Yellow

                         }
                    }
               }

               # Show summary of duplicates if any found
               if ( $duplicateNames.Count -gt 0 ) {
                    Write-Host "`nDUPLICATE CONFIGURATIONS DETECTED:" -ForegroundColor Red
                    $separatorLine = "=" * 35
                    Write-Host $separatorLine -ForegroundColor Red
                    foreach ( $dupName in $duplicateNames ) {
                         $dupConfigs = $AllResults | Where-Object { $_.ConfigName.ToLower() -eq $dupName }
                         Write-Host "`n'$dupName' found in:" -ForegroundColor Yellow
                         foreach ( $dupConfig in $dupConfigs ) {
                              Write-Host "- $($dupConfig.ConfigPath) ($([System.IO.Path]::GetFileName($dupConfig.ConfigFile)))" -ForegroundColor Gray
                         }
                    }
                    Write-Host "`nConsider removing duplicate configurations to avoid conflicts." -ForegroundColor Yellow
               }

          }

     } catch {

          Write-Error "Error in Get-tpcAvailableCounterConfig: $($_.Exception.Message)"

     }

}