- Joined
- Dec 18, 2024
- Messages
- 18
- Reaction score
- 11
Hey y'all, sharing out my script to get the max concurrent calls for v20. You need to have your CDR enabled and in single file format for this script. Since there is no report or status on this usage as of today. You can download the cdr.log from your on-prem PBX @ /var/lib/3cxpbx/Instance1/Data/Logs/CDRLogs/ - Run the PS1 from the same directory as the downloaded cdr.log file.
*Edit, appears some things changed in u7, I'll make a separate post when I adjust script for that one in the next day.
*Edit, appears some things changed in u7, I'll make a separate post when I adjust script for that one in the next day.
Code:
# 3CX v20 CDR Analyzer (FAST, 1-second progress, starts-before-ends, reliable peak time)
# Assumes standard v20 layout: start = column 3, end = column 5 (0-based), unquoted CSV.
param(
[string]$Path = "cdr.log",
[int]$ProgressIntervalSeconds = 1
)
# ---------------- CONFIG ----------------
[string[]]$formats = @('yyyy/MM/dd HH:mm:ss')
$culture = [System.Globalization.CultureInfo]::InvariantCulture
$style = [System.Globalization.DateTimeStyles]::None
# Resolve relative path safely
$fullPath = Join-Path -Path (Get-Location) -ChildPath $Path
if (-not (Test-Path $fullPath)) {
Write-Error "CDR file not found: $fullPath"
exit
}
# Field indices for 3CX v20 (0-based)
$startIdx = 3
$endIdx = 5
# ---------------- HELPERS ----------------
function Get-Field {
param([string]$line, [int]$index)
$len = $line.Length; $start = 0; $commaCount = 0
for ($i = 0; $i -lt $len; $i++) {
if ($line[$i] -eq ',') {
if ($commaCount -eq $index) { return $line.Substring($start, $i - $start) }
$commaCount++; $start = $i + 1
}
}
if ($commaCount -eq $index) { return $line.Substring($start) }
return $null
}
function Parse-Time {
param([string]$s)
if ([string]::IsNullOrWhiteSpace($s)) { return $null }
[datetime]$out = [datetime]::MinValue
if ([datetime]::TryParseExact($s, $formats, $culture, $style, [ref]$out)) { $out } else { $null }
}
# ---------------- STATE ----------------
# Separate hashtables so we apply starts BEFORE ends at each timestamp
$startsAt = New-Object System.Collections.Hashtable 200000
$endsAt = New-Object System.Collections.Hashtable 200000
[datetime]$minStart = [datetime]::MaxValue
[datetime]$maxEnd = [datetime]::MinValue
$haveStart = $false
$haveEnd = $false
$maxConcurrent = 0
$currentConcurrent = 0
[datetime]$peakTimestamp = [datetime]::MinValue # sentinel instead of Nullable
# ---------------- MAIN LOOP (FAST + 1s PROGRESS) ----------------
$fs = [System.IO.File]::OpenRead($fullPath)
try {
$sr = New-Object System.IO.StreamReader($fs)
$totalBytes = $fs.Length
$lines = 0
$lastTick = [datetime]::UtcNow
while (($line = $sr.ReadLine()) -ne $null) {
$lines++
if ([string]::IsNullOrWhiteSpace($line)) { continue }
# Time-based progress update (~every 1 second)
if (([datetime]::UtcNow - $lastTick).TotalSeconds -ge $ProgressIntervalSeconds) {
$lastTick = [datetime]::UtcNow
$pct = if ($totalBytes -gt 0) { [math]::Round(($fs.Position / $totalBytes) * 100, 2) } else { 0 }
Write-Progress -Activity "Analyzing 3CX CDR..." -Status "Processed $lines lines" -PercentComplete $pct
}
# Extract start/end (assumes unquoted CSV)
$startStr = Get-Field -line $line -index $startIdx
$endStr = Get-Field -line $line -index $endIdx
# Parse and accumulate
$start = Parse-Time $startStr
if ($start) {
if ($startsAt.ContainsKey($start)) { $startsAt[$start] += 1 } else { $startsAt[$start] = 1 }
if ($start -lt $minStart) { $minStart = $start; $haveStart = $true }
}
$end = Parse-Time $endStr
if ($end) {
if ($endsAt.ContainsKey($end)) { $endsAt[$end] += 1 } else { $endsAt[$end] = 1 }
if ($end -gt $maxEnd) { $maxEnd = $end; $haveEnd = $true }
}
}
}
finally {
if ($sr) { $sr.Close() }
if ($fs) { $fs.Close() }
}
# ---------------- SWEEP (STARTS BEFORE ENDS + RELIABLE PEAK TIME) ----------------
# Force keys to true [datetime] before sorting (prevents string-typing edge cases)
$allTs = ($startsAt.Keys + $endsAt.Keys) | ForEach-Object { if ($_ -is [datetime]) { $_ } else { [datetime]$_ } } | Sort-Object -Unique
foreach ($ts in $allTs) {
if ($startsAt.ContainsKey($ts)) { $currentConcurrent += $startsAt[$ts] } # starts first
if ($currentConcurrent -gt $maxConcurrent) {
$maxConcurrent = $currentConcurrent
$peakTimestamp = $ts
}
if ($endsAt.ContainsKey($ts)) { $currentConcurrent -= $endsAt[$ts] } # then ends
}
Write-Progress -Activity "Analyzing 3CX CDR..." -Completed -Status "Done!"
# ---------------- OUTPUT ----------------
$fmt = 'yyyy/MM/dd HH:mm:ss'
$windowStart = if ($haveStart) { $minStart.ToString($fmt) } else { '(unknown)' }
$windowEnd = if ($haveEnd) { $maxEnd.ToString($fmt) } else { '(unknown)' }
Write-Host "---------------------------------------------"
Write-Host (" Max concurrent calls : {0}" -f $maxConcurrent)
if ($peakTimestamp -ne [datetime]::MinValue) {
Write-Host (" Peak first occurred : {0}" -f $peakTimestamp.ToString($fmt))
} else {
Write-Host " Peak first occurred : (not found)"
}
Write-Host (" Time window analyzed : {0} → {1}" -f $windowStart, $windowEnd)
Write-Host (" Lines processed : {0}" -f $lines)
Write-Host "---------------------------------------------"
Last edited: