Public/Start-SCOMTrace.ps1

Function Start-SCOMTrace
{
  <#
  .Synopsis
    This will initiate SCOM agent tracing, output trace files, then format the trace files into .log for humans.
 
  .EXAMPLE
    Start-SCOMTrace -General -Verbose
  .EXAMPLE
    Start-SCOMTrace -General -TraceSeconds 600 -GeneralGuidLevel Extended -OutPath C:\TraceLogs -Verbose
  .EXAMPLE
    Start-SCOMTrace -Specific -TraceSeconds 600 -SpecificTraceName 'MyCustomTrace' -OutPath C:\TraceLogs -Verbose -MaxLogMB 2048
 
  .NOTES
  Function: Start-SCOMTrace
  Author: Tyson Paul (https://monitoringguys.com/)
  Version History:
  2024.01.31.1441 - Fixed tmf cab expansion command.
  2021.01.21.11047 - Rewrite into proper function for SCOMAgentHelper module
  2020.11.20.1727 - v1
 
  #>



  Param (
    # the duration of the trace
    [Parameter(Mandatory = $false,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false,
        ValueFromRemainingArguments = $false)]
    [Alias('Duration')]
    [int]$TraceSeconds = 300,


    <#
        'Specific' requires workflow override of 'TraceEnabled' property for specific object instance (not group or class).
 
        ------- Override Example -------
 
        <Monitoring>
        <Overrides>
        <MonitorPropertyOverride ID="WorkflowDebugger.WorkflowTraceOverride" Context="SQLServer!Microsoft.SQLServer.Windows.DBEngine" ContextInstance="2c821943-be80-5733-0b16-59850426eabe" Enforced="false" Monitor="TempDBPerformance!TempDBPerformance.TempDBPercentUsage.Monitor" Property="TraceEnabled">
        <Value>true</Value>
        </MonitorPropertyOverride>
        </Overrides>
        </Monitoring>
 
        ------- End Override Example -------
    #>

    [Parameter(Mandatory = $true,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false,
                   ParameterSetName='Parameter Set 1')]
    [switch]$Specific,


    # Name of the logfile. Used for a Specific trace scenario only.
    [Parameter(Mandatory = $false,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false,
                   ParameterSetName='Parameter Set 1')]
    [string]$SpecificTraceName = 'MyCustomWorkflowTrace',


    # General trace for common workflow types
    [Parameter(Mandatory = $true,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false,
                   ParameterSetName='Parameter Set 2')]
    [switch]$General,


    # Which GUIDs to include in the capture. Used for General trace scenario only.
    [Parameter(Mandatory = $false,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false,
                   ParameterSetName='Parameter Set 2')]
    [ValidateSet('Basic', 'Extended', 'Full')]
    [string]$GeneralGuidLevel = 'Basic',


    #Max size of log files (circular)
    [Parameter(Mandatory = $false,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false)]
    [int]$MaxLogMB = 1024,


    # This is the path to the agent Tools folder. Use only if the default path is broken or incorrect in the registry.
    [Parameter(Mandatory = $false,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false)]
    [string]$AgentToolsPath = 'NONE',


    # Where to output the trace files
    [Parameter(Mandatory = $false,
        ValueFromPipeline = $false,
        ValueFromPipelineByPropertyName = $false)]
    [string]$OutputPath = 'C:\Windows\Logs\OpsMgrTrace'

  )

  #==============================================================================

  # Basic is most common. It's rare to need/use Extended or Full. This hash is a clever way to control tiers of inclusion.
  $hashGuidFileNames = [ordered]@{}
  Switch ($GeneralGuidLevel) {
    {
      $_ -match 'Basic|Extended|Full'
    }
    {
      $hashGuidFileNames['TracingGuidsNative'] = 'TracingGuidsNative.txt'
      $hashGuidFileNames['TracingGuidsScript'] = 'TracingGuidsScript.txt'
      $hashGuidFileNames['TracingGuidsManaged'] = 'TracingGuidsManaged.txt'
    }

    {
      $_ -match 'Extended|Full'
    }
    {
      $hashGuidFileNames['TracingGuidsConfigService'] = 'TracingGuidsConfigService.txt'
      $hashGuidFileNames['TracingGuidsDAS'] = 'TracingGuidsDAS.txt'
      $hashGuidFileNames['TracingGuidsFailover'] = 'TracingGuidsFailover.txt'
      $hashGuidFileNames['TracingGuidsUI'] = 'TracingGuidsUI.txt'
    }

    'Full'
    {
      $hashGuidFileNames['TracingGuidsAdvisor'] = 'TracingGuidsAdvisor.txt'
      $hashGuidFileNames['TracingGuidsAPM'] = 'TracingGuidsAPM.txt'
      $hashGuidFileNames['TracingGuidsApmConnector'] = 'TracingGuidsApmConnector.txt'
      $hashGuidFileNames['TracingGuidsBID'] = 'TracingGuidsBID.txt'
      $hashGuidFileNames['TracingGuidsNASM'] = 'TracingGuidsNASM.txt'
    }
  }

  If (-NOT (Test-Path $OutputPath -PathType Container)){
    New-Item -Path $OutputPath -ItemType Directory -ErrorAction SilentlyContinue
  }

  If (-NOT(Test-Path -Path $AgentToolsPath -PathType Container -ErrorAction SilentlyContinue) )
  {
    # Locate tools directory
    $setupKey = Get-Item -Path 'HKLM:\Software\Microsoft\Microsoft Operations Manager\3.0\Setup'
    $InstallDirectory = $setupKey.GetValue('InstallDirectory')
    $AgentToolsPath = Join-Path -Path $InstallDirectory -ChildPath 'Tools'
  }

  Set-Location $AgentToolsPath

  Write-Output "`nAny active traces will first be stopped.`n"
  # Stop any previous tracing
  & .\Stoptracing.cmd

  Write-Output "`n`n`n"

  If ($General ) {
    ForEach ($Key in @($hashGuidFileNames.Keys))
    {
      $outFile = (Join-Path -Path $OutputPath -ChildPath "$($Key).etl")
      Write-Output "Start General trace session for $outFile.`n"
      # Start general trace
      .\TraceLogSM.exe -start $Key -flag 0x1F -level 6 -f $outFile -b 64 -ft 10 -cir $MaxLogMB -guid $($hashGuidFileNames[$Key])
    }
  }

  If ($Specific) {
    # This hash variable is used for the stop/format commands at the end.
    $hashGuidFileNames = [ordered]@{
      $SpecificTraceName = $SpecificTraceName
    }
    $MySpecificTraceFile = Join-Path -Path $OutputPath -ChildPath "$($SpecificTraceName).etl"
    Write-Output "Start Specific trace session: $SpecificTraceName.`n"
    # Start trace for specific workflow object instance
    & .\TraceLogSM.exe -start $SpecificTraceName -flag 0xFF -level 5 -ft 1 -rt -GUID '#c85ab4ed-7f0f-42c7-8421-995da9810fdd' -b 1024 -f $MySpecificTraceFile
  }


  Write-Output "`n`nTracing for $TraceSeconds seconds..."
  $EndTime = (Get-Date).AddSeconds($TraceSeconds)
  $TMFExists = $false

  While ((Get-Date) -le $EndTime) {
    If (((Get-Date).Second)%10 -eq 0)
    {
      Write-Output "`n$([math]::Max(([int]($EndTime - (Get-Date)).TotalSeconds),0) ) seconds remain until trace completion..."
    }

    If (-NOT $TMFExists){
      # Ensure that 'all.tmf' exists in Tools folder
      $allPath = (Join-Path $AgentToolsPath 'all.tmf')
      If (-NOT (Test-Path -Path $allPath -PathType Leaf)) {
        Write-Output "`nIn the meantime, 'all.tmf' not found at this path: [$($allPath)]. No problem, I ($(whoami.exe)) will fix it now."
        Write-Output "Will extract all .cabs in the TMF folder. Will concatenate all .tmf files from the .\TMF\*.tmf path into a single file: .\all.tmf"
        $CABs = @(Get-ChildItem -Path (Join-Path $AgentToolsPath 'TMF') -Filter '*.cab')
        ForEach ($Cab in $CABs) {
          expand.exe $CAB.FullName -F:*.tmf ".\tmf\$($Cab.BaseName).tmf"
        }
        Write-Output "`nBuilding 'all.tmf' file..."
        Get-Content .\tmf\*.tmf | Out-File .\all.tmf -Encoding utf8

        If (-NOT (Test-Path -Path $allPath -PathType Leaf)) {
          Write-Output "`nFailed to create necessary tmf file at path: [$($allPath)]."
        }
        Else {
          Write-Output "`nGood news! 'all.tmf' now exists at this path: [$($allPath)]."
          $TMFExists = $True
        }
      }
      Else {
        Write-Output "`nGood news! 'all.tmf' exists at this path: [$($allPath)]."
        $TMFExists = $True
      }
    }

    Start-Sleep -Seconds 1
  }

  # If script has been stopped manually for some reason, no problem! You can run the below commands to stop the TRACES and format the logs.
  # Stop tracing session and format logs for humans to read
  @($hashGuidFileNames.Keys) | ForEach-Object -Process {
    Write-Output "`n`nStopping trace of session: $_."
    & .\TraceLogSM.exe -stop $_
  }

  @($hashGuidFileNames.Keys) | ForEach-Object -Process {
    Write-Output "`n`nFormating: $(Join-Path -Path $OutputPath -ChildPath "$($_).etl")..."
    # Format the trace data into plain text for humans
    & .\TraceFmtSM.exe $(Join-Path -Path $OutputPath -ChildPath "$($_).etl") -tmf .\all.tmf -o $(Join-Path -Path $OutputPath -ChildPath "$($_).LOG")
  }
  Write-Output "`nFormat done!`n"
}