One of the oldest and most useful tools to figure out what's going on in your Active Directory environment is dcdiag. Dcdiag is a command-line utility that comes with Windows. It allows administrators to run various diagnostic checks against their Active Directory environments.

In the earlier 2000s, a command-line utility that returned dumb strings would have been OK, but today we have smart objects with PowerShell! Let's build a wrapper around the dcdiag tool with PowerShell to bring this handy tool into the current times.

If you run dcdiag by itself, you can see that it returns some useful information, but this is just loose text. Dcdiag does not return objects we can parse and manipulate as we can in PowerShell.

Use DcDiag with PowerShell to check domain controller health

Use DcDiag with PowerShell to check domain controller health

To wrap this tool in PowerShell, we'll need to build a function. I'll call my function Invoke-DcDiag. We can get crazy with this, but to keep it simple, I want to pass a single domain controller (DC) to dcdiag, so I'll create this function with a parameter called DomainController.

function Invoke-DcDiag {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$DomainController
    )

    ## Code goes here
}

Once I've built the skeleton, I can then add in the code. To run dcdiag against a domain controller, I need to use the /s switch, so I'll add that to my function. I need to capture the output, so I'll assign it to a variable. To mimic the code that will go into the function, I'll assign $DomainController a value of DC representing the name of the DC in my environment.

$DomainController = 'DC'
$result = dcdiag /s:$DomainController

I've now captured the output to a variable. That's great and all, but we now need to parse the output. Let's get our regex on! Before we get that far though, I need to define what the output should look like. Eyeballing our sample output, it seems like for every test that happens, dcdiag always returns one of two strings:

<DomainControllerName> passed test <TestName>
<DomainControllerName> failed test <TestName>

I want PowerShell to return an object for each test result with a TestName, TestResult, and an EntityName to represent a domain controller, NT Directory Service (NTDS) partition, or whatever else the test is running against.

We'll need to parse each line in the string. One way to do this is to use Select-String. Select-String has a parameter called Pattern that allows us to try to match a string against a regular expression. Without teaching you regex, I'll give you the answer here. The regex you need for this is '\. (.*) \b(passed|failed)\b test (.*)'.

When you run it as is, you may think that either I'm crazy or it doesn't work. One of these things is right.

PS> $dcDiagOutput | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)'

         ......................... DC passed test Connectivity
         ......................... DC passed test Advertising
         ......................... DC passed test FrsEvent
         ......................... DC passed test DFSREvent
         ......................... DC passed test SysVolCheck
         ......................... DC passed test KccEvent
         ......................... DC passed test KnowsOfRoleHolders

However, PowerShell is hiding regex groups from you. Each regex group contains each of the properties we'd like the resulting output to have. We reference these groups by looking at the various indexes in the Groups array that the Matches property contains that Select-String returns.

You can see below that I'm looping over each match, assigning each group value to an object property we'd like, and then returning the object.

$result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
    $obj = @{
        TestName = $_.Matches.Groups[3].Value
        TestResult = $_.Matches.Groups[2].Value
        Entity = $_.Matches.Groups[1].Value
    }
    [pscustomobject]$obj
}

Bringing this code all back into our function makes the function look like this:

function Invoke-DcDiag {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$DomainController
    )

    $result = dcdiag /s:$DomainController
    $result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
        $obj = @{
            TestName = $_.Matches.Groups[3].Value
            TestResult = $_.Matches.Groups[2].Value
            Entity = $_.Matches.Groups[1].Value
        }
        [pscustomobject]$obj
    }
}

At this time, we bring this function into our session, run it via Invoke-DcDiag -DomainController DC, and voila, dcdiag for the modern age!

Subscribe to 4sysops newsletter!

PS> Invoke-DcDiag -DomainController DC

TestResult Entity          TestName
---------- ------          --------
passed     DC              Connectivity
passed     DC              Advertising
passed     DC              FrsEvent
passed     DC              DFSREvent
passed     DC              SysVolCheck
passed     DC              KccEvent
passed     DC              KnowsOfRoleHolders
passed     DC              MachineAccount
passed     DC              NCSecDesc
passed     DC              NetLogons
passed     DC              ObjectsReplicated
passed     DC              Replications
passed     DC              RidManager
passed     DC              Services
passed     DC              SystemLog
passed     DC              VerifyReferences
passed     ForestDnsZones  CheckSDRefDom
passed     DomainDnsZones  CheckSDRefDom
passed     Schema          CheckSDRefDom
passed     Schema          CrossRefValidation
passed     Configuration   CheckSDRefDom
passed     Configuration   CrossRefValidation
passed     techsnips       CheckSDRefDom
passed     techsnips       CrossRefValidation
passed     techsnips.local LocatorCheck
passed     techsnips.local Intersite
avataravataravataravataravatar
27 Comments
  1. Frank 3 years ago

    Hi,

    nice script!!1

    How can I put the whole Output to the Body of an email?

    Frank

    • Hi Frank,

      there is no straight-forward way. Send-EmailMessage accepts only type String as Body and this is customobject. You would need to convert it to string, but that would loose the format and the output would be unreadable.

      Guess you need to create a HTML code from the output and pass it to Body parameter together with -BodyAsHTML switch.

      Leos

      avatar
    • @Frank

      You can try this:

      $Body = Invoke-DcDiag -DomainController DC | ConvertTo-Html
      
      $Params = @{
          'SmtpServer'  = 'smtp.mycompany.com'
          'Port'        =  25
          'Priority'    = 'Normal'
          'From'        = 'sender@mycompany.com'
          'To'          = 'mainrecipient@mycompany.com'
          'Cc'          = 'copyrecipient@mycompany.com'
          'Bcc'         = 'hiddenrecipient@mycompany.com'
          'Subject'     = 'Mail title'
          'Body'        =  $Body
          'BodyAsHtml'  =  $true
      } 
      Send-MailMessage @Params

      • Hi Luc,

        I tried that, the output is a System.Collections.Hashtable and my output was something like this

        IsReadOnly	IsFixedSize	IsSynchronized	Keys	Values	SyncRoot	Count
        False	False	False	System.Collections.Hashtable+KeyCollection	System.Collections.Hashtable+ValueCollection

    • @Frank

      Leos is right, I forgot the Out-String cmdlet.
      Here is the fixed code, which I have tested.

      $Body = Invoke-DcDiag -DomainController DC | ConvertTo-Html | Out-String
      
      $Params = @{
          'SmtpServer' = 'smtp.mycompany.com'
          'Port'       = 25
          'Priority'   = 'Normal'
          'From'       = 'sender@mycompany.com'
          'To'         = 'mainrecipient@mycompany.com'
          'Cc'         = 'copyrecipient@mycompany.com'
          'Bcc'        = 'hiddenrecipient@mycompany.com'
          'Subject'    = 'Mail title'
          'Body'       = $Body
          'BodyAsHtml' = $true
      }
      Send-MailMessage @Params

      Thanks Leos for your eagle eye!

      avatar
      • Frank 3 years ago

        Hi all,

        thx for your anwers!

        I not want to use the whole function. Only sorted Output for:

        $result = dcdiag /s:$DomainController
        $result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
              $obj = @{
                Entity = $_.Matches.Groups[1].Value
                TestResult = $_.Matches.Groups[2].Value
                TestName = $_.Matches.Groups[3].Value
               }
              
         }
        $Body = [pscustomobject]$obj | Sort-Object -Property TestResult | ConvertTo-Html -Head $style | Out-String

        But i will not work.Ayn Idea?

        Frank

        • Frank 3 years ago

          Got it right...;-)

          $result = dcdiag /s:$DomainController
          $result | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
              $obj = "" | Select-Object TestResult,TestName,Target
              $obj.Target             = $_.Matches.Groups[1].Value
              $obj.TestResult     = $_.Matches.Groups[2].Value
              $obj.TestName     = $_.Matches.Groups[3].Value
              $Output += $obj 
              }

          $Body = $Output | Sort-Object -Property TestResult | ConvertTo-Html -Head $style | Out-String

          avatar
  2. Manlio 3 years ago

    Hi, can you help me to understand why I don't receive any output?

    after imported the function correctly, I run "Invoke-DcDiag -DomainController " but the output is empty.

    PS C:\Windows\system32> Invoke-DcDiag -DomainController <HIDDEN-DC>
    
    PS C:\Windows\system32> 

  3. Manlio 3 years ago

    It seems to be a problem with this part of the script select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' 
    because if I change the "-pattern" filter it works... 

    • @Manlio

      What's the output when you run the command directly?

      dcdiag /s:$DomainController

      • Manlio 3 years ago

        @Luke Fullenwarth

         

        the output is correct as it should be , but the issue seems to be the "select-string" filter that's not working correctly, without that and applying another kind of "filter" it works.

         

        avatar
  4. Pascal 3 years ago

    Giving back, I added a few things

    • Parameter Validation (to ensure the DC is truly a DC)
    • By default the DCList is auto populated of every RWDCs
    • Run DCDiag with verbose output
    • Run DCDiag on DCs in parallel instead of sequential
    • Adds the full log as a by default hidden property
    function Invoke-DcDiag {
        param(
            [Parameter(Mandatory=$False)]
            [ValidateScript({
    		$EAP = $ErrorActionPreference
    		$retval = $true
    		$ErrorActionPreference = 'Stop'
    		Try{ ForEach($DCName in $_) { Get-ADDomainController $DCName } } Catch { $retval = $false; Throw "$DCName doesn't exist or other error" } Finally { $ErrorActionPreference = $EAP }	
    		$retval
    	    })]
            [string[]]$DomainControllers = $(Get-ADDomainController -Filter { IsReadOnly -ne $true } | Select -ExpandProperty Name)
        )
        Begin {
    #Ref:  http://blogs.microsoft.co.il/scriptfanatic/2012/04/13/custom-objects-default-display-in-powershell-30/
            [string[]]$DefaultProperties = 'DomainController','Entity','TestName','TestResult'
            $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]$(New-Object System.Management.Automation.PSPropertySet DefaultDisplayPropertySet,$DefaultProperties)
        }
        Process {
            $DCDiagResults = Invoke-Command -ScriptBlock { dcdiag /v } -ComputerName $DomainControllers
            ForEach ($DCDiagResult in $DCDiagResults | Group-Object -Property PSComputerName ) {
                $DCDiagResult.Group | select-string -pattern '\. (.*) \b(passed|failed)\b test (.*)' | foreach {
                    [pscustomobject]@{
                        DomainController = $DCDiagResult.Name
                        Entity = $_.Matches.Groups[1].Value
                        TestName = $_.Matches.Groups[3].Value
                        TestResult = $_.Matches.Groups[2].Value
                        FullLog = $DCDiagResult.Group -join "`n"
                    } | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers -PassThru  # With PassThru will also return data to pipeline
                }
            }
        }
    }

  5. Vandrey Trindade 3 years ago

    Hi,

    My results are in portuguese. It isn't returning nothing, I have changed the "passed|failed" and still doesn't work.

    Here is a sample of my dcdiag command:

     

    Executando testes de partição em : axxiom1
    
          Iniciando teste: CheckSDRefDom
    
             ......................... axxiom1 passou no teste CheckSDRefDom
    
          Iniciando teste: CrossRefValidation
    
             ......................... axxiom1 passou no teste CrossRefValidation
    

    • @Vandrey

      You must replace english words with portugues words in the following command (line 9 of the function):

      select-string -pattern '. (.*) b(passed|failed)b test (.*)'

      Probably you have to replace:

      • passed
      • failed
      • test

      • Vandrey Trindade 3 years ago

        @Luc,

        Yup.

        I've tried that... but the problem is that in portuguese the "failed" translation is "não passou" (like 'not passed').

        So it breaks the script. But no problem! Thanks for helping!

        • @Vandrey

          How does it brake the script? What's the error message?

          • Vandrey Trindade 3 years ago

            @Luc,

             

            Sorry for the delay... I got the dengue fever and I'm returning to work only today.

            There was no error, the problem was that since the translation to portuguese used two separated words, each word appeared on different columns.

            I'll try to run it on the server directly, it's on English language.

            Thanks!

            • Vandrey Trindade 3 years ago

              Worked as expected directly on the server. Thanks!

              avatar
  6. Marcelo (Rank: 1)
    3 years ago

    Hi, I added this simple modification for use if running on DC
    $DC = (Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
    Invoke-DcDiag -DomainController $DC

    • @Marcelo

      Yes, that works but is a bit overkill...

      There is an environment variable which stores the computer name.

      $DC = $env:COMPUTERNAME

      Usually, I set the ComputerName variable with the local computer name as the default value.

      param(
          [Parameter(Mandatory)]
          [ValidateNotNullOrEmpty()]
          [string]$ComputerName = $env:COMPUTERNAME
      )

      Thus when you want to run a script or function on the local computer you don't need to specify the ComputerName parameter.

  7. Manlio 3 years ago

    If use the last version by Pascal, it works fine!! yes

    thank you

  8. damien 2 years ago

    Hi,

    Thanks for your help. In french it's à little different cause there are several line for test name + result

     Démarrage du test : Advertising
             ......................... Le test Advertising
              de DC a réussi
          Démarrage du test : FrsEvent
             ......................... Le test FrsEvent
              de DC a réussi

     

    How to catch it ?

    • Hi Damien, Please check Luc's solution above in the comment section(Vandrey Trindade's comment).  

    • Sojef 8 months ago

      Try to write dcdiag output via /f to log file and put it into $result via get-content. This worked for german output.

      &dcdiag /f:$env:TEMP\dcdiag.log
      $result = Get-Content $env:TEMP\dcdiag.log

  9. damien 2 years ago

    That doesn't work cause in french result is on 2 lines and not in 1 line like english or portugese

    • Understood. You would need some modification in regex expression. Will get back to you as I complete testing new regex in my lab.

  10. JaJa 1 year ago

    Excellent script and suggestions. Just a thought about DNS using /test:dns. Can we incorporate that some how?

Leave a reply to Pascal Click here to cancel the reply

Please enclose code in pre tags

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2021

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account