Using the LDAP stats control with Powershell
At TEC this year I did a session with Darren the "GPO Guy" regarding using S.DS.Protocols to get back query stats from Active Directory. I mentioned my stats control code several times and I promised I would post the code so here it is:
What: The stats control is a LDAP control that you can pass that will tell the server to return its internal stats on a query.
Why: The stats control is a great way to see what the Domain Controller does with your filter. Like what indexes it hits, how many entries it had to visit, how much time the DC spent, and entries visited. It is very useful in creating the most efficient LDAP Query possible.
How: I Used System.DirectoryServices.Protocols.DirectoryControl to pass the LDAP control to the Server and I used System.DirectoryServices.Protocols.BERConverter along with the protocol spec here: LDAP_SERVER_GET_STATS_OID: 1.2.840.113556.1.4.970 to decode the Byte Array that was returned.
Here is what is Returned: For 2000
- threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
- coreTime: The time in milliseconds which core logic in the DC spent processing the request.
- callTime: The overall time in milliseconds that the DC spent processing the request.
- searchSubOperations: The number of individual operations which the DC performed in processing the request.
For 2003/2008
- threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
- callTime: The overall time in milliseconds that the DC spent processing the request
- entriesReturned: The number of objects returned in the search result.
- entriesVisited: The number of objects that the DC considered for inclusion in the search result.
- filter: String which represents the optimized form of the search filter used by the DC to perform a search. This very well may be different than the filter that was passed.
- index: String which indicates which database indexes were used by the DC to perform the search.
For 2008 Only
- pagesReferenced: The number of database pages referenced by the DC in processing the search.
- pagesRead: The number of database pages read from disk.
- pagesPreread: The number of database pages preread from disk by the DC in processing the search.
- pagesDirtied: The number of clean database pages modified by the DC in processing the search.
- pagesRedirtied: The number of previously modified database pages that were modified by the DC in processing the search.
- logRecordCount: The number of database log records generated by the DC in processing the search.
- logRecordBytes: The size in bytes of database log records generated by the DC in processing the search.
Note:
- Must have SE_DEBUG_PRIVILEGE
- I did NOT implement SO_EXTENDED_FMT flag.
- I did NOT test 2000.
- The functions that decodes Byte Array actually return objects, but for this test I just outputed the test to mimic ADFind.exe
- Special thanks to Robin Caron, joe Richards, and Dmitri Gavrilov for help with the decoding.
- Here is GREAT Doc on the Controls (and everything else AD) [MS-ADTS]: Active Directory Technical Specification
Code:
What: The stats control is a LDAP control that you can pass that will tell the server to return its internal stats on a query.
Why: The stats control is a great way to see what the Domain Controller does with your filter. Like what indexes it hits, how many entries it had to visit, how much time the DC spent, and entries visited. It is very useful in creating the most efficient LDAP Query possible.
How: I Used System.DirectoryServices.Protocols.DirectoryControl to pass the LDAP control to the Server and I used System.DirectoryServices.Protocols.BERConverter along with the protocol spec here: LDAP_SERVER_GET_STATS_OID: 1.2.840.113556.1.4.970 to decode the Byte Array that was returned.
Here is what is Returned: For 2000
- threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
- coreTime: The time in milliseconds which core logic in the DC spent processing the request.
- callTime: The overall time in milliseconds that the DC spent processing the request.
- searchSubOperations: The number of individual operations which the DC performed in processing the request.
For 2003/2008
- threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
- callTime: The overall time in milliseconds that the DC spent processing the request
- entriesReturned: The number of objects returned in the search result.
- entriesVisited: The number of objects that the DC considered for inclusion in the search result.
- filter: String which represents the optimized form of the search filter used by the DC to perform a search. This very well may be different than the filter that was passed.
- index: String which indicates which database indexes were used by the DC to perform the search.
For 2008 Only
- pagesReferenced: The number of database pages referenced by the DC in processing the search.
- pagesRead: The number of database pages read from disk.
- pagesPreread: The number of database pages preread from disk by the DC in processing the search.
- pagesDirtied: The number of clean database pages modified by the DC in processing the search.
- pagesRedirtied: The number of previously modified database pages that were modified by the DC in processing the search.
- logRecordCount: The number of database log records generated by the DC in processing the search.
- logRecordBytes: The size in bytes of database log records generated by the DC in processing the search.
Note:
- Must have SE_DEBUG_PRIVILEGE
- I did NOT implement SO_EXTENDED_FMT flag.
- I did NOT test 2000.
- The functions that decodes Byte Array actually return objects, but for this test I just outputed the test to mimic ADFind.exe
- Special thanks to Robin Caron, joe Richards, and Dmitri Gavrilov for help with the decoding.
- Here is GREAT Doc on the Controls (and everything else AD) [MS-ADTS]: Active Directory Technical Specification
Code:
Param( $filter = "(objectclass=*)", $base, $Server, [int]$pageSize = 1000, [string[]]$props = @("1.1"), [switch]$StatsOnly, [switch]$Verbose ) function CreateStatsObject2008{ Param($StatsArray) $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaiaiiiiiiiiiiiiii}",$StatsArray) # Win2008 $myStatsObject = New-Object System.Object $myStatsObject | Add-Member -Name "ThreadCount" -Value $DecodedArray[1] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "CallTime" -Value $DecodedArray[3] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "EntriesVisited" -Value $DecodedArray[7] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "Filter" -Value $DecodedArray[9] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "Index" -Value $DecodedArray[11] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "PagesReferenced" -Value $DecodedArray[13] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "PagesRead" -Value $DecodedArray[15] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "PagesPreread" -Value $DecodedArray[17] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "PagesDirtied" -Value $DecodedArray[19] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "PagesRedirtied" -Value $DecodedArray[21] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "LogRecordCount" -Value $DecodedArray[23] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "LogRecordBytes" -Value $DecodedArray[25] -MemberType "NoteProperty" $myStatsObject } function CreateStatsObject2003{ Param($StatsArray) $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaia}",$StatsArray) # Win2003 $myStatsObject = New-Object System.Object $myStatsObject | Add-Member -Name "ThreadCount" -Value $DecodedArray[1] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "CallTime" -Value $DecodedArray[3] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "EntriesVisited" -Value $DecodedArray[7] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "Filter" -Value $DecodedArray[9] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "Index" -Value $DecodedArray[11] -MemberType "NoteProperty" $myStatsObject } function CreateStatsObject2000{ Param($StatsArray) $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiii}",$StatsArray) # Win2000 $myStatsObject = New-Object System.Object $myStatsObject | Add-Member -Name "ThreadCount" -Value $DecodedArray[1] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "CoreTime" -Value $DecodedArray[3] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "CallTime" -Value $DecodedArray[5] -MemberType "NoteProperty" $myStatsObject | Add-Member -Name "searchSubOperations" -Value $DecodedArray[7] -MemberType "NoteProperty" $myStatsObject } if($Verbose){$VerbosePreference\3 = "Continue"} Write-Verbose " - Loading System.DirectoryServices.Protocols" [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") [int]$pageCount = 0 [int]$objcount = 0 if(!$Server) { $rootDSE = [ADSI]"LDAP://rootDSE" $Server = $rootDSE.dnsHostName if(!$base){$base = $rootDSE.defaultNamingContext} switch ($rootDSE.domainControllerFunctionality) { 0 {$expression = 'CreateStatsObject2000 $stats'} 2 {$expression = 'CreateStatsObject2003 $stats'} 3 {$expression = 'CreateStatsObject2008 $stats'} } } Write-Verbose " - Using Server: [$Server]" Write-Verbose " - Using Base: [$base]" Write-Verbose " - Using Filter: [$filter]" Write-Verbose " - Page Size: [$PageSize]" Write-Verbose " - Returning: [$props]" Write-Verbose " - CSV: [$csv]" Write-Verbose " - NoHeaders: [$noHeader]" Write-Verbose " - Count: [$Count]" Write-Verbose " - StatsOnly: [$StatsOnly]" Write-Verbose " - Expression: [$expression]" Write-Verbose " - Creating LDAP connection Object" $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server) $Subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree" Write-Verbose " + Creating SearchRequest Object" $SearchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$Subtree,$props) Write-Verbose " - Creating System.DirectoryServices.Protocols.PageResultRequestControl Object" $PagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize) Write-Verbose " - Creating System.DirectoryServices.Protocols.SearchOptionsControl Object" $SearchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope) Write-Verbose " - Creating System.DirectoryServices.Protocols.DirectoryControl Control for OID: [1.2.840.113556.1.4.970]" $oid = "1.2.840.113556.1.4.970" $StatsControl = New-Object System.DirectoryServices.Protocols.DirectoryControl($oid,$null,$false,$true) Write-Verbose " - Adding Controls" [void]$SearchRequest.Controls.add($pagedRequest) [void]$SearchRequest.Controls.Add($searchOptions) [void]$SearchRequest.Controls.Add($StatsControl) $start = Get-Date while ($True) { # Increment the pageCount by 1 $pageCount++ # Cast the directory response into a SearchResponse object Write-Verbose " - Cast the directory response into a SearchResponse object" $searchResponse = $connection.SendRequest($searchRequest) # Display the retrieved page number and the number of directory entries in the retrieved page Write-Verbose (" - Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count) Write-Verbose " - Returning Stats for Page:$PageCount" $stats = $searchResponse.Controls[0].GetValue() $ResultStats = invoke-Expression $expression if($pageCount -eq 1) { $StatsFilter = $ResultStats.Filter $StatsIndex = $ResultStats.Index Write-Verbose " + Setting Filter to [$StatsFilter]" Write-Verbose " + Setting Index to [$StatsIndex]" } # If Cookie Length is 0, there are no more pages to request" if ($searchResponse.Controls[1].Cookie.Length -eq 0) { if($count){$objcount} "`nStatistics" "=================================" "Elapsed Time: {0} (ms)" -f ((Get-Date).Subtract($start).TotalMilliseconds) "Returned {0} entries of {1} visited - ({2})`n" -f $ResultStats.EntriesReturned,$ResultStats.EntriesVisited,($ResultStats.EntriesReturned/$ResultStats.EntriesVisited).ToString('p') "Used Filter:" "- {0}`n" -f $StatsFilter "Used Indices:" "- {0}`n" -f $StatsIndex break } # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next # page of data in the send request and cast the directory control into a PageResultResponseControl object Write-Verbose " - Setting Cookie on SearchResponse to the PageReQuest" $pagedRequest.Cookie = $searchResponse.Controls[1].Cookie }
tshell :: Apr.09.2009 :: Active Directory, All :: No Comments »

