Get-PartitionsReport.psm1
#Requires -Modules Az.Storage #Requires -PSEdition Core function Get-PartitionsReport { <# .SYNOPSIS Create/update a .csv report based on consumer group blobs and an Azure EventHub (or Azure IoTHub) and return a list PartitionCompare objects. .DESCRIPTION Create/update a .csv report based on consumer group blobs and an Azure EventHub (or Azure IoTHub) and return a list PartitionCompare objects. The .csv file name will be in the form eventhubname_yyyyMMdd.csv .PARAMETER EventHubConnectionString The EventHub connection string to query. Format: Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=myKeyName;SharedAccessKey=mySharedAccessKey;EntityPath=myeventHubname .PARAMETER StorageName The StorageName that contains the consumer group folder of the processor. .PARAMETER StorageKey The primary or secondary key of the storage containing the consumer group blobs. .PARAMETER ContainerName The container name containing the consumer group blobs. .PARAMETER ConsumerGroupFolder The consumer group name used by event processor. .PARAMETER OutputPath The output path where the .csv report will be saved/updated. .PARAMETER InputFile The absolute or relative path of the input file containing required parameters. .PARAMETER Clipboard Switch to export result to clipboard. Available on Windows. .EXAMPLE # Create a report on directory C:\Export\ getting parameter from file C:\users\<username>\inputFile.txt and copy the result to clipboard. Get-Partitions -InputFile C:\users\<username>\inputFile.txt -OutputPath "C:\Export\" -Clipboard (Clipboard option available on Windows Platform) .EXAMPLE # Create a report on directory C:\Export\ Get-Partitions -EventHubConnectionString <EventHubConnectionString> -StorageName <StorageName> ` -StorageKey <StorageKey> -ContainerName <ContainerName> -ConsumerGroupFolder <ConsumerGroupFolder> -OutputPath "C:\Export\" #> [OutputType('PartitionCompare')] [cmdletbinding(DefaultParameterSetName='InputFile')] Param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')] [Alias("EH", "EventHub", "EHConnString")] [string] $EventHubConnectionString, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')] [Alias("SN")] [string] $StorageName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')] [Alias("SK")] [string] $StorageKey, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')] [Alias("Container")] [string] $ContainerName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')] [Alias("ConsumerGroup", "CG")] [string] $ConsumerGroupFolder, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')] [Alias("OutputDir")] [string] $OutputPath, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='InputFile')] [Alias("Input")] [string] $InputFile ) DynamicParam { if($IsWindows) { # Begin dynamic parameter definition $ParamName_Clipboard = 'Clipboard' $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $false $ParameterAttribute.ValueFromPipelineByPropertyName = $true $ParameterAttribute.ValueFromPipeline = $true $AttributeCollection.Add($ParameterAttribute) $ParamAlias = New-Object System.Management.Automation.AliasAttribute -ArgumentList "Clip" $AttributeCollection.Add($ParamAlias) <# [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [Alias("Clip")] [switch] $Clipboard $ValidationValues = Get-CsOnlineTelephoneNumber -IsNotAssigned | Select -ExpandProperty Id $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ValidationValues) $AttributeCollection.Add($ValidateSetAttribute) #> # End Dynamic parameter definition # When done building dynamic parameters, return # Set up the Run-Time Parameter Dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_Clipboard, [System.Management.Automation.SwitchParameter], $AttributeCollection) $RuntimeParameterDictionary.Add($ParamName_Clipboard, $RuntimeParameter) return $RuntimeParameterDictionary } } begin { if($PSCmdlet.ParameterSetName -eq "InputFile"){ if(!(Test-Path -Path $InputFile -PathType Leaf)) { throw ("Input File '$InputFile' doesn't exists or it is a directory") } else{ $inputFileContent = (Get-Content $InputFile) -split "`n" $EventHubConnectionString = ($inputFileContent | Where-Object {$_.StartsWith("EventHubConnectionString=")}).Trim().TrimEnd(";") $StorageName = ($inputFileContent | Where-Object {$_.StartsWith("StorageName=")}).Trim() $pattern = "StorageName=(\w+)" $regex = [Regex]::new($pattern) $match = $regex.Match($StorageName) $StorageName = $match.Groups[1].ToString() $StorageKey = ($inputFileContent | Where-Object {$_.StartsWith("StorageKey=")}).Trim() $pattern = "StorageKey=([^;|^`n]*)" $regex = [Regex]::new($pattern) $match = $regex.Match($StorageKey) $StorageKey = $match.Groups[1].ToString() $ContainerName = ($inputFileContent | Where-Object {$_.StartsWith("ContainerName=")}).Trim() $pattern = "ContainerName=(\w+)" $regex = [Regex]::new($pattern) $match = $regex.Match($ContainerName) $ContainerName = $match.Groups[1].ToString() $ConsumerGroupFolder = ($inputFileContent | Where-Object {$_.StartsWith("ConsumerGroupFolder=")}).Trim() $pattern = "ConsumerGroupFolder=(\w+)" $regex = [Regex]::new($pattern) $match = $regex.Match($ConsumerGroupFolder) $ConsumerGroupFolder = $match.Groups[1].ToString() $OutputPath = ($inputFileContent | Where-Object {$_.StartsWith("OutputPath=")}).Trim() $pattern = "OutputPath=([^`n]*)" $regex = [Regex]::new($pattern) $match = $regex.Match($OutputPath) $OutputPath = $match.Groups[1].ToString() } } } process { function Get-SasToken(){ Param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [string] $URI, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [string] $KeyName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [string] $Key, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [int] $ExpiresIn = 600 ) [Reflection.Assembly]::LoadWithPartialName("System.Web")| out-null #Token defauls expires now+600 $Expires=([DateTimeOffset]::Now.ToUnixTimeSeconds())+$ExpiresIn $SignatureString=[System.Web.HttpUtility]::UrlEncode($URI)+ "`n" + [string]$Expires $HMAC = New-Object System.Security.Cryptography.HMACSHA256 $HMAC.key = [Text.Encoding]::ASCII.GetBytes($Key) $Signature = $HMAC.ComputeHash([Text.Encoding]::ASCII.GetBytes($SignatureString)) $Signature = [Convert]::ToBase64String($Signature) $SASToken = "sr=" + [System.Web.HttpUtility]::UrlEncode($URI) + "&sig=" + [System.Web.HttpUtility]::UrlEncode($Signature) + "&se=" + $Expires + "&skn=" + $KeyName return $SASToken } class RestPartition { [ValidateNotNullOrEmpty()][int] $PartitionID [ValidateNotNullOrEmpty()][long] $SizeInBytes [ValidateNotNullOrEmpty()][long] $BeginSequenceNumber [ValidateNotNullOrEmpty()][long] $EndSequenceNumber [ValidateNotNullOrEmpty()][long] $IncomingBytesPerSecond [ValidateNotNullOrEmpty()][long] $OutgoingBytesPerSecond [ValidateNotNullOrEmpty()][long] $LastEnqueuedOffset [ValidateNotNullOrEmpty()][DateTime] $LastEnqueuedTimeUtc RestPartition([int] $partitionID, [long] $SizeInBytes, [long] $BeginSequenceNumber, [long] $EndSequenceNumber, [long] $IncomingBytesPerSecond, [long] $OutgoingBytesPerSecond, [long] $LastEnqueuedOffset, [DateTime] $LastEnqueuedTimeUtc) { $this.PartitionID = $PartitionID $this.SizeInBytes = $SizeInBytes $this.BeginSequenceNumber = $BeginSequenceNumber $this.EndSequenceNumber = $EndSequenceNumber $this.IncomingBytesPerSecond = $IncomingBytesPerSecond $this.OutgoingBytesPerSecond = $OutgoingBytesPerSecond $this.LastEnqueuedOffset = $LastEnqueuedOffset $this.LastEnqueuedTimeUtc = $LastEnqueuedTimeUtc } } class PartitionBlob { [ValidateNotNullOrEmpty()][int] $PartitionID [string] $Owner [ValidateNotNullOrEmpty()][long] $Epoch [ValidateNotNullOrEmpty()][long] $BlobSequenceNumber [ValidateNotNullOrEmpty()][DateTime] $LastModifiedBlobUTC PartitionBlob([int] $PartitionID, [string] $Owner, [long] $Epoch, [long] $BlobSequenceNumber, [DateTime] $LastModifiedBlobUTC) { $this.PartitionID = $PartitionID $this.Owner = $Owner $this.Epoch = $Epoch $this.BlobSequenceNumber = $BlobSequenceNumber $this.LastModifiedBlobUTC = $LastModifiedBlobUTC } } class PartitionCompare { [ValidateNotNullOrEmpty()][long] $RunID [ValidateNotNullOrEmpty()][int] $PartitionID [ValidateNotNullOrEmpty()][long] $Difference [string] $Owner [ValidateNotNullOrEmpty()][long] $ProcessorSequence [ValidateNotNullOrEmpty()][long] $EventHubSequence [ValidateNotNullOrEmpty()][DateTime] $LastModifiedBlobUTC [ValidateNotNullOrEmpty()][DateTime] $EventHubLastEnqueuedUTC PartitionCompare([long] $RunID,[int] $PartitionID, [string] $Owner, [long] $ProcessorSequence, [long] $EventHubSequence, [DateTime] $LastModifiedBlobUTC, [DateTime] $EventHubLastEnqueuedUTC) { $this.RunID = $RunID $this.PartitionID = $PartitionID $this.Owner = $Owner $this.ProcessorSequence = $ProcessorSequence $this.EventHubSequence = $EventHubSequence $this.Difference = $EventHubSequence - $ProcessorSequence $this.LastModifiedBlobUTC = $LastModifiedBlobUTC $this.EventHubLastEnqueuedUTC = $EventHubLastEnqueuedUTC } PartitionCompare([long] $RunID, [PartitionBlob] $partitionBlob, [RestPartition] $restPartition) { $this.RunID = $RunID $this.PartitionID = $partitionBlob.PartitionID $this.Owner = $partitionBlob.Owner $this.ProcessorSequence = $partitionBlob.BlobSequenceNumber $this.EventHubSequence = $restPartition.EndSequenceNumber $this.Difference = $this.EventHubSequence - $this.ProcessorSequence $this.LastModifiedBlobUTC = $partitionBlob.LastModifiedBlobUTC $this.EventHubLastEnqueuedUTC = $restPartition.LastEnqueuedTimeUtc } } $EventHubConnectionString = $EventHubConnectionString.TrimEnd(";") $tempFolder = [System.IO.Path]::GetTempPath() $tempGUID = [System.Guid]::NewGuid().ToString("N") $consumerTempFolder = Join-Path -Path $tempFolder -ChildPath $tempGUID -AdditionalChildPath $ConsumerGroupFolder if(!(Test-path $consumerTempFolder)){ New-Item -ItemType Directory -Path $consumerTempFolder | Out-Null } $pattern = "Endpoint=sb://([^/]*)" $regex = [Regex]::new($pattern) $match = $regex.Match($EventHubConnectionString) $eventhubNamespace = $match.Groups[1].ToString() $pattern = "EntityPath=(.*)" $regex = [Regex]::new($pattern) $match = $regex.Match($EventHubConnectionString) $eventhubName = $match.Groups[1].ToString() if($eventhubNamespace.StartsWith("iothub-ns")){ $FilePrefix = $eventhubName } else{ $pattern = "([^\.]*)" $regex = [Regex]::new($pattern) $match = $regex.Match($eventhubNamespace) $FilePrefix = $match.Groups[1].ToString() } $pattern = "SharedAccessKeyName=(\w+)" $regex = [Regex]::new($pattern) $match = $regex.Match($EventHubConnectionString) $keyName = $match.Groups[1].ToString() $pattern = "SharedAccessKey=([^;]*)" $regex = [Regex]::new($pattern) $match = $regex.Match($EventHubConnectionString) $key = $match.Groups[1].ToString() Write-Verbose "`tEventHub/IoTHub NameSpace: $eventhubNamespace" Write-Verbose "`tEventHub Name: $eventhubName" Write-Verbose "`tStorage: $StorageName" Write-Verbose "`tContainer: $ContainerName" Write-Verbose "`tConsumer Group: $ConsumerGroupFolder`n" if(!($EventHubConnectionString -and $eventhubNamespace -and $eventhubName -and $ConsumerGroupFolder -and $ContainerName -and $StorageName -and $StorageKey)) { Write-Verbose "Parameters parse failed" throw ("Input parameters wrong format") } $REST_Uri = [String]::Format("https://{0}/{1}/consumergroups/{2}/partitions?api-version=2015-01", $eventhubNamespace, $eventhubName, '$Default') $SasToken = Get-SasToken -URI $REST_Uri -KeyName $keyName -Key $key $header = @{ 'Authorization'= "SharedAccessSignature $SasToken" 'Content-Type' = 'application/atom+xml;type=entry;charset=utf-8' } $response = Invoke-RestMethod -Method Get -Uri $REST_Uri -Headers $header $StorageContext = New-AzStorageContext -StorageAccountName $StorageName -StorageAccountKey $StorageKey #ListBlobs can be skipped knowing the name of the partition #$ConsumerBlobs = Get-AzStorageBlob -Prefix $ConsumerGroupFolder -Container $ContainerName -Context $StorageContext #Removing old temporary files Remove-Item -Path $consumerTempFolder\* -Force #Temporary Arrays $PartitionsBlobs = [System.Collections.ArrayList]::new() $RestPartitions = [System.Collections.ArrayList]::new() $partitionCount = $response.Count Write-Host "`nStart analysis $partitionCount partitions`n" for($i = 0; $i -lt $partitionCount;$i++){ $startElaboration = Get-Date #Calculate percentage $i:$partitionCount=Percentage:100 Write-Progress -Activity "Getting partition $($i+1) of $partitionCount" -Status "$([System.Math]::Floor((($i*100)/$partitionCount)))% Complete:" -PercentComplete ([System.Math]::Floor((($i*100)/$partitionCount))) #Retrieving Eventhub information Write-Host "`tQuerying partition: #$i" $EH_Rest_Uri_SinglePartition = [String]::Format("https://{0}/{1}/consumergroups/{2}/partitions/{3}?api-version=2015-01", $eventhubNamespace, $eventhubName, '$Default',$i) $SasToken = Get-SasToken -URI $EH_Rest_Uri_SinglePartition -KeyName $keyName -Key $key $header = @{ 'Authorization'= "SharedAccessSignature $SasToken" 'Content-Type' = 'application/atom+xml;type=entry;charset=utf-8' } $response = Invoke-RestMethod -Method Get -Uri $EH_Rest_Uri_SinglePartition -Headers $header $Description = $response.entry.content.PartitionDescription $PartitionID = $response.entry.title.'#text' $BeginSequenceNumber = [long]::Parse($Description.BeginSequenceNumber) $EndSequenceNumber = [long]::Parse($Description.EndSequenceNumber) $IncomingBytesPerSecond = [long]::Parse($Description.IncomingBytesPerSecond) $OutgoingBytesPerSecond = [long]::Parse($Description.OutgoingBytesPerSecond) $LastEnqueuedOffSet = [long]::Parse($Description.LastEnqueuedOffset) $LastEnqueuedTimeUtc = ([Datetime]::Parse($Description.LastEnqueuedTimeUtc)).ToUniversalTime() $SizeInBytes = [long]::Parse($Description.SizeInBytes) $RestPartitionObj = [RestPartition]::new($PartitionID,$SizeInBytes, $BeginSequenceNumber,$EndSequenceNumber,$IncomingBytesPerSecond,$OutgoingBytesPerSecond,$LastEnqueuedOffSet,$LastEnqueuedTimeUtc) $RestPartitions.Add($RestPartitionObj) | Out-Null #Retrieving Blob information Write-Host "`tDownloading blob: $ConsumerGroupFolder/$i" do{ $Failed = $false Try{ $blob = Get-AzStorageBlob -Blob "$ConsumerGroupFolder/$i" -Container $ContainerName -Context $StorageContext #-EA Stop to catch Exception StorageException and retry to dowload blob failed $BlobFullPath = Join-Path -Path $consumerTempFolder -ChildPath $i Get-AzStorageBlobContent -Blob "$ConsumerGroupFolder/$i" -Container $ContainerName -Context $StorageContext -Force -Destination $BlobFullPath -EA Stop | Out-Null } catch [System.Exception] { Write-Verbose "`tRetrying..." $Failed = $true } } while ($Failed) $blobContent = Get-Content -Path $BlobFullPath $blobJson = ConvertFrom-Json -InputObject $blobContent $PartitionsBlobObj = [PartitionBlob]::new([int]::Parse($blobJson.PartitionId),$blobJson.Owner,[long]::Parse($blobJson.Epoch),[long]::Parse($blobJson.SequenceNumber),$blob.LastModified.ToUniversalTime().UtcDateTime) $PartitionsBlobs.Add($PartitionsBlobObj) | Out-Null $endElaboration = Get-Date $ts = New-TimeSpan -Start $startElaboration -End $endElaboration $elapsedString = [string]::Format("The total elapsed time to analyze partition #$i is: {0:c}", $ts) Write-Verbose $elapsedString Write-Host "`tDifference partition #$i`: $($RestPartitionObj.EndSequenceNumber-$PartitionsBlobObj.BlobSequenceNumber)`n" } $PartitionsCompare = [System.Collections.Generic.List[PartitionCompare]]::new() $OutputFile = $FilePrefix + "_"+ (Get-date -Format "yyyyMMdd") $RunID = Get-date -Format "yyyyMMddhhmm" for ($ID = 0; $ID -lt $PartitionsBlobs.Count; $ID++) { $RestPart = $RestPartitions | Where-Object {$_.PartitionID -eq $ID } $BlobPart = $PartitionsBlobs | Where-Object {$_.PartitionID -eq $ID } $PartitionsCompare.add([PartitionCompare]::new([long]::Parse($RunID),$BlobPart,$RestPart)) | Out-Null } $OutputPath = $OutputPath.TrimEnd("/") If(!(Test-Path -Path $OutputPath)){ New-Item -Path $OutputPath -ItemType Directory | Out-Null } if(!(Test-path (Join-Path -Path $OutputPath -ChildPath "$OutputFile.csv"))){ New-item -ItemType File -Path $OutputPath -Name "$OutputFile.csv" | Out-Null "RunID,PartitionID,Difference,Owner,ProcessorSequence,EventHubSequence,LastModifiedBlobUTC,EventHubLastEnqueuedUTC" | Out-File -FilePath ((Join-Path -Path $OutputPath -ChildPath "$OutputFile,csv")) -Append } $PartitionsCompare | Export-Csv -Path (Join-Path -Path $OutputPath -ChildPath "$OutputFile,csv") -Append -NoTypeInformation if($Clipboard.IsPresent){ ($PartitionsCompare | Sort-Object -Property PartitionID | ConvertTo-Csv -NoTypeInformation).Replace(",","`t") | clip } return $PartitionsCompare } end { #Removing temporary files Remove-Item -Path $consumerTempFolder\* -Force } } Export-ModuleMember -Function Get-PartitionsReport |