The script below monitors LDAP operations on a Domain Controller and logs detailed information about queries that exceed specified thresholds for execution time, CPU usage, or results returned. It helps identify problematic LDAP queries that may be impacting domain controller performance.
Parameter: ThresholdSeconds
Minimum query duration in seconds to log (default: 5)
Parameter: LogPath
Path where log files will be saved (default: C:\LDAPDiagnostics)
Parameter: MonitorDuration
How long to monitor in minutes (default: continuous)
EXAMPLE
.\Diagnose-LDAPQueries.ps1 -ThresholdSeconds 3 -LogPath “C:\Logs\LDAP”
[CmdletBinding()]
param(
[int]$ThresholdSeconds = 5,
[string]$LogPath = "C:\LDAPDiagnostics",
[int]$MonitorDuration = 0 # 0 = continuous
)
# Requires Administrator privileges
#Requires -RunAsAdministrator
# Create log directory if it doesn't exist
if (-not (Test-Path $LogPath)) {
New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
}
$logFile = Join-Path $LogPath "LDAP_Diagnostics_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$csvFile = Join-Path $LogPath "LDAP_Queries_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Level] $Message"
Write-Host $logMessage
Add-Content -Path $logFile -Value $logMessage
}
function Get-LDAPStatistics {
try {
# Query NTDS performance counters for LDAP statistics
$ldapStats = @{
ActiveThreads = (Get-Counter '\NTDS\LDAP Active Threads' -ErrorAction SilentlyContinue).CounterSamples.CookedValue
SearchesPerSec = (Get-Counter '\NTDS\LDAP Searches/sec' -ErrorAction SilentlyContinue).CounterSamples.CookedValue
ClientSessions = (Get-Counter '\NTDS\LDAP Client Sessions' -ErrorAction SilentlyContinue).CounterSamples.CookedValue
BindTime = (Get-Counter '\NTDS\LDAP Bind Time' -ErrorAction SilentlyContinue).CounterSamples.CookedValue
}
return $ldapStats
}
catch {
Write-Log "Error getting LDAP statistics: $_" "ERROR"
return $null
}
}
function Parse-LDAPEvent {
param($Event)
$eventData = @{
TimeCreated = $Event.TimeCreated
ClientIP = $null
ClientPort = $null
StartingNode = $null
Filter = $null
SearchScope = $null
AttributeSelection = $null
ServerControls = $null
VisitedEntries = $null
ReturnedEntries = $null
TimeInServer = $null
}
# Parse event XML for detailed information
try {
$xml = [xml]$Event.ToXml()
$dataNodes = $xml.Event.EventData.Data
foreach ($node in $dataNodes) {
switch ($node.Name) {
"Client" { $eventData.ClientIP = ($node.'#text' -split ':')[0] }
"StartingNode" { $eventData.StartingNode = $node.'#text' }
"Filter" { $eventData.Filter = $node.'#text' }
"SearchScope" { $eventData.SearchScope = $node.'#text' }
"AttributeSelection" { $eventData.AttributeSelection = $node.'#text' }
"ServerControls" { $eventData.ServerControls = $node.'#text' }
"VisitedEntries" { $eventData.VisitedEntries = $node.'#text' }
"ReturnedEntries" { $eventData.ReturnedEntries = $node.'#text' }
"TimeInServer" { $eventData.TimeInServer = $node.'#text' }
}
}
}
catch {
Write-Log "Error parsing event XML: $_" "WARNING"
}
return $eventData
}
Write-Log "=== LDAP Query Diagnostics Started ===" "INFO"
Write-Log "Threshold: $ThresholdSeconds seconds" "INFO"
Write-Log "Log Path: $LogPath" "INFO"
Write-Log "Monitor Duration: $(if($MonitorDuration -eq 0){'Continuous'}else{$MonitorDuration + ' minutes'})" "INFO"
# Enable Field Engineering logging if not already enabled
Write-Log "Checking Field Engineering diagnostic logging settings..." "INFO"
try {
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics"
$currentValue = Get-ItemProperty -Path $regPath -Name "15 Field Engineering" -ErrorAction SilentlyContinue
if ($currentValue.'15 Field Engineering' -lt 5) {
Write-Log "Enabling Field Engineering logging (level 5)..." "INFO"
Set-ItemProperty -Path $regPath -Name "15 Field Engineering" -Value 5
Write-Log "Field Engineering logging enabled. You may need to restart NTDS service for full effect." "WARNING"
}
else {
Write-Log "Field Engineering logging already enabled at level $($currentValue.'15 Field Engineering')" "INFO"
}
}
catch {
Write-Log "Error configuring diagnostic logging: $_" "ERROR"
}
# Create CSV header
$csvHeader = "TimeCreated,ClientIP,StartingNode,Filter,SearchScope,AttributeSelection,VisitedEntries,ReturnedEntries,TimeInServer,ServerControls"
Set-Content -Path $csvFile -Value $csvHeader
Write-Log "Monitoring for expensive LDAP queries (threshold: $ThresholdSeconds seconds)..." "INFO"
Write-Log "Press Ctrl+C to stop monitoring" "INFO"
$startTime = Get-Date
$queriesLogged = 0
try {
while ($true) {
# Check if monitoring duration exceeded
if ($MonitorDuration -gt 0) {
$elapsed = (Get-Date) - $startTime
if ($elapsed.TotalMinutes -ge $MonitorDuration) {
Write-Log "Monitoring duration reached. Stopping." "INFO"
break
}
}
# Get current LDAP statistics
$stats = Get-LDAPStatistics
if ($stats) {
Write-Verbose "Active Threads: $($stats.ActiveThreads), Searches/sec: $($stats.SearchesPerSec), Client Sessions: $($stats.ClientSessions)"
}
# Query Directory Service event log for expensive LDAP queries
# Event ID 1644 = expensive search operations
$events = Get-WinEvent -FilterHashtable @{
LogName = 'Directory Service'
Id = 1644
StartTime = (Get-Date).AddSeconds(-10)
} -ErrorAction SilentlyContinue
foreach ($event in $events) {
$eventData = Parse-LDAPEvent -Event $event
# Convert time in server from milliseconds to seconds
$timeInSeconds = if ($eventData.TimeInServer) {
[int]$eventData.TimeInServer / 1000
} else {
0
}
if ($timeInSeconds -ge $ThresholdSeconds) {
$queriesLogged++
Write-Log "=== Expensive LDAP Query Detected ===" "WARNING"
Write-Log "Time: $($eventData.TimeCreated)" "WARNING"
Write-Log "Client IP: $($eventData.ClientIP)" "WARNING"
Write-Log "Duration: $timeInSeconds seconds" "WARNING"
Write-Log "Starting Node: $($eventData.StartingNode)" "WARNING"
Write-Log "Filter: $($eventData.Filter)" "WARNING"
Write-Log "Search Scope: $($eventData.SearchScope)" "WARNING"
Write-Log "Visited Entries: $($eventData.VisitedEntries)" "WARNING"
Write-Log "Returned Entries: $($eventData.ReturnedEntries)" "WARNING"
Write-Log "Attributes: $($eventData.AttributeSelection)" "WARNING"
Write-Log "Server Controls: $($eventData.ServerControls)" "WARNING"
Write-Log "======================================" "WARNING"
# Write to CSV
$csvLine = "$($eventData.TimeCreated),$($eventData.ClientIP),$($eventData.StartingNode),`"$($eventData.Filter)`",$($eventData.SearchScope),`"$($eventData.AttributeSelection)`",$($eventData.VisitedEntries),$($eventData.ReturnedEntries),$($eventData.TimeInServer),`"$($eventData.ServerControls)`""
Add-Content -Path $csvFile -Value $csvLine
}
}
Start-Sleep -Seconds 5
}
}
catch {
Write-Log "Error during monitoring: $_" "ERROR"
}
finally {
Write-Log "=== LDAP Query Diagnostics Stopped ===" "INFO"
Write-Log "Total expensive queries logged: $queriesLogged" "INFO"
Write-Log "Log file: $logFile" "INFO"
Write-Log "CSV file: $csvFile" "INFO"
}
```
## Usage Examples
### Basic Usage (Continuous Monitoring)
Run with default settings - monitors queries taking 5+ seconds:
```powershell
.\Diagnose-LDAPQueries.ps1
```
### Custom Threshold and Duration
Monitor for 30 minutes, logging queries that take 3+ seconds:
```powershell
.\Diagnose-LDAPQueries.ps1 -ThresholdSeconds 3 -MonitorDuration 30
```
### Custom Log Location
Save logs to a specific directory:
```powershell
.\Diagnose-LDAPQueries.ps1 -LogPath "D:\Logs\LDAP"
```
### Verbose Output
See real-time LDAP statistics while monitoring:
```powershell
.\Diagnose-LDAPQueries.ps1 -Verbose
```
## Requirements
- **Administrator privileges** on the domain controller
- **Windows Server** with Active Directory Domain Services role
- **PowerShell 5.1 or later**
## Understanding the Output
### Log File Example
```
[2025-01-15 14:23:45] [WARNING] === Expensive LDAP Query Detected ===
[2025-01-15 14:23:45] [WARNING] Time: 01/15/2025 14:23:43
[2025-01-15 14:23:45] [WARNING] Client IP: 192.168.1.50
[2025-01-15 14:23:45] [WARNING] Duration: 8.5 seconds
[2025-01-15 14:23:45] [WARNING] Starting Node: DC=contoso,DC=com
[2025-01-15 14:23:45] [WARNING] Filter: (&(objectClass=user)(memberOf=*))
[2025-01-15 14:23:45] [WARNING] Search Scope: 2
[2025-01-15 14:23:45] [WARNING] Visited Entries: 45000
[2025-01-15 14:23:45] [WARNING] Returned Entries: 12000
```
### What to Look For
- **High visited/returned ratio** - Indicates an inefficient filter
- **Subtree searches from root** - Often unnecessarily broad
- **Wildcard filters** - Like `(cn=*)` can be very expensive
- **Unindexed attributes** - Queries on non-indexed attributes visit many entries
- **Repeated queries** - Same client making the same expensive query repeatedly
## Troubleshooting Common Issues
### No Events Appearing
If you're not seeing Event ID 1644, you may need to lower the expensive search threshold in Active Directory:
```powershell
# Lower the threshold to 1000ms (1 second)
Get-ADObject "CN=Query-Policies,CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=yourdomain,DC=com" |
Set-ADObject -Replace @{lDAPAdminLimits="MaxQueryDuration=1000"}
```
### Script Requires Restart
After enabling Field Engineering logging, you may need to restart the NTDS service:
```powershell
Restart-Service NTDS -Force
```
Best Practices
1. **Run during peak hours** to capture real-world problematic queries
2. **Start with a lower threshold** (2-3 seconds) to catch more queries
3. **Analyze the CSV** in Excel or Power BI for patterns
4. **Correlate with client IPs** to identify problematic applications
5. **Work with application owners** to optimize queries with indexes or better filters
Once you’ve identified expensive queries:
1. **Add indexes** for frequently searched attributes
2. **Optimize LDAP filters** to be more specific
3. **Reduce search scope** where possible
4. **Implement paging** for large result sets
5. **Cache results** on the client side when appropriate
This script has helped me identify numerous performance bottlenecks in production environments. I hope it helps you optimize your Active Directory infrastructure as well!