Get-PartitionsReport.psm1

#Requires -Modules Az.Storage
#Requires -PSEdition Core, Desktop
#Requires -Version 5.1
function Get-PartitionsReport {
<#
 .SYNOPSIS
  Create/update a .csv report based on consumer group blobs and an Azure EventHub (or Azure IoTHub) and return a PartitionCompare/PartitionBlob objects list.
 
 .DESCRIPTION
  Create/update a .csv report based on consumer group blobs and an Azure EventHub (or Azure IoTHub) and return a PartitionCompare/PartitionBlob objects list.
  The .csv file name will be in the form eventhubname_yyyyMMdd.csv or eventhubnamePO_yyyyMMdd.csv in case of ProcessorOnly switch
   
 .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.
  You can find an example of the input file here: https://github.com/Stereo89/Azereo/blob/master/Get-PartitionsReport/inputTemplate.txt
   
  .PARAMETER ProcessorOnly
  Switch to get only Processor info (on storage).
 
 .EXAMPLE
   # Create a report on directory C:\Export\ getting parameter from file C:\users\<username>\inputFile.txt
   Get-PartitionsReport -InputFile C:\users\<username>\inputFile.txt -OutputPath "C:\Export\"
 
 .EXAMPLE
   # Create a report on directory C:\Export\
   Get-PartitionsReport -EventHubConnectionString <EventHubConnectionString> -StorageName <StorageName> `
        -StorageKey <StorageKey> -ContainerName <ContainerName> -ConsumerGroupFolder <ConsumerGroupFolder> -OutputPath "C:\Export\"
  
 .EXAMPLE
   # Create a report of the Processor blobs status on directory C:\Export\ getting parameter from file C:\users\<username>\inputFile.txt
   Get-PartitionsReport -InputFile C:\users\<username>\inputFile.txt -OutputPath "C:\Export\" -ProcessorOnly
 
 .OUTPUTS
  A .csv file (file name will be in the form eventhubname_yyyyMMdd.csv or eventhubnamePO_yyyyMMdd.csv in case of ProcessorOnly switch)
  PartitionBlob objects list in case of -ProcessorOnly switch
  PartitionCompare object list if -ProcessorOnly switch is NOT used
 
 .LINK
  https://github.com/Stereo89/Azereo/tree/master/Get-PartitionsReport
 
#>

    
    [cmdletbinding(DefaultParameterSetName='InputFile')]
    [OutputType('PartitionCompare', ParameterSetName="Nofile")]
    [OutputType('PartitionCompare', ParameterSetName="InputFile")]
    [OutputType('PartitionBlob', ParameterSetName="ProcessorNoFile")]
    [OutputType('PartitionBlob', ParameterSetName="ProcessorWithFile")]
    
    Param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorNoFile')]
        [Alias("EH", "EventHub", "EHConnString")]
        [string] $EventHubConnectionString,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorNoFile')]
        [Alias("SN")]
        [string] $StorageName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorNoFile')]
        [Alias("SK")]
        [string] $StorageKey,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorNoFile')]
        [Alias("Container")]
        [string] $ContainerName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='NoFile')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorNoFile')]
        [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')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorWithFile')]
        [Alias("Input")]
        [string] $InputFile,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorNoFile')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, ParameterSetName='ProcessorWithFile')]
        [System.Management.Automation.SwitchParameter] $ProcessorOnly 
    )

    <# Removing until a clip command will be available on all platforms
    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)
                 
                <#
 
                Notes:
                . 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)
 
 
 
                [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" -or $PSCmdlet.ParameterSetName -eq "ProcessorWithFile" ){
                
                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=([^;|^`n]*)"
                    $regex = [Regex]::new($pattern)
                    $match = $regex.Match($ContainerName)
                    $ContainerName = $match.Groups[1].ToString()

                    $ConsumerGroupFolder = ($inputFileContent | Where-Object {$_.StartsWith("ConsumerGroupFolder=")}).Trim()
                    $pattern = "ConsumerGroupFolder=([^;|^`n]*)"
                    $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()][long] $BlobOffset
                [ValidateNotNullOrEmpty()][DateTime] $LastModifiedBlobUTC

                PartitionBlob([int] $PartitionID, [string] $Owner, [long] $Epoch, [long] $BlobSequenceNumber, [long] $BlobOffset, [DateTime] $LastModifiedBlobUTC)
                {
                    $this.PartitionID = $PartitionID
                    $this.Owner = $Owner
                    $this.Epoch = $Epoch
                    $this.BlobSequenceNumber = $BlobSequenceNumber
                    $this.BlobOffset = $BlobOffset
                    $this.LastModifiedBlobUTC = $LastModifiedBlobUTC
                }

            }

            class PartitionCompare
            {
                [ValidateNotNullOrEmpty()][long] $RunID
                [ValidateNotNullOrEmpty()][int] $PartitionID
                [ValidateNotNullOrEmpty()][long] $Difference
                [string] $Owner
                [ValidateNotNullOrEmpty()][long] $ProcessorSequence
                [ValidateNotNullOrEmpty()][long] $EventHubSequence
                [ValidateNotNullOrEmpty()][long] $ProcessorOffset
                [ValidateNotNullOrEmpty()][long] $EventHubOffset
                [ValidateNotNullOrEmpty()][DateTime] $LastModifiedBlobUTC
                [ValidateNotNullOrEmpty()][DateTime] $EventHubLastEnqueuedUTC

                PartitionCompare([long] $RunID,[int] $PartitionID, [string] $Owner, [long] $ProcessorSequence, [long] $EventHubSequence, [long] $ProcessorOffset, [long] $EventHubOffset, [DateTime] $LastModifiedBlobUTC, [DateTime] $EventHubLastEnqueuedUTC)
                {
                    $this.RunID = $RunID
                    $this.PartitionID = $PartitionID
                    $this.Owner = $Owner
                    $this.ProcessorSequence = $ProcessorSequence
                    $this.EventHubSequence = $EventHubSequence
                    $this.Difference = $EventHubSequence - $ProcessorSequence
                    $this.$ProcessorOffset = $ProcessorOffset
                    $this.$EventHubOffset = $EventHubOffset
                    $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.ProcessorOffset = $partitionBlob.BlobOffset
                    $this.EventHubOffset = $restPartition.LastEnqueuedOffset
                    $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 (Join-Path -Path $tempGUID -ChildPath $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()
            }

            if($ProcessorOnly.IsPresent){
                $FilePrefix+="PO"
            }

            $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"
                Write-Verbose "$EventHubConnectionString`n$eventhubNamespace`n$eventhubName`n$ConsumerGroupFolder`n$ContainerName`n$StorageName"
                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.Generic.List[PartitionBlob]]::new()
            $RestPartitions = [System.Collections.Generic.List[RestPartition]]::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)))
                
                if(!($ProcessorOnly.IsPresent)){
                    #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..."
                        Start-Sleep -Seconds 1
                        $Failed = $true
                    }
                } while ($Failed)

                $blobContent = Get-Content -Path $BlobFullPath
                $blobJson = ConvertFrom-Json -InputObject $blobContent
                if($blobJson.SequenceNumber -lt $RestPartitionObj.BeginSequenceNumber){$blobJson.SequenceNumber = $RestPartitionObj.BeginSequenceNumber}
                if([string]::IsNullOrEmpty($blobJson.Offset)) {$blobJson.Offset = "0"}
                $PartitionsBlobObj = [PartitionBlob]::new([int]::Parse($blobJson.PartitionId),$blobJson.Owner,[long]::Parse($blobJson.Epoch),[long]::Parse($blobJson.SequenceNumber),[long]::Parse($blobJson.Offset), $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
                if(!($ProcessorOnly.IsPresent)){
                    Write-Host "`tDifference partition #$i`: $($RestPartitionObj.EndSequenceNumber-$PartitionsBlobObj.BlobSequenceNumber)`n"
                }
                
            }
            if(!($ProcessorOnly.IsPresent)){
                $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,ProcessorOffset,EventHubOffset,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
                
                <#Removing until clip command will be available on all platform
                if($Clipboard.IsPresent){
                    ($PartitionsCompare | Sort-Object -Property PartitionID | ConvertTo-Csv -NoTypeInformation).Replace(",","`t") | clip
                }
                #>

                return $PartitionsCompare
            }
            else{
                $OutputFile = $FilePrefix + "_"+ (Get-date -Format "yyyyMMdd")
                $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
                    "PartitionID,Owner,Epoch,BlobSequenceNumber,Offset,LastModifiedBlobUTC" | Out-File -FilePath ((Join-Path -Path $OutputPath -ChildPath "$OutputFile.csv")) -Append
                }

                $PartitionsBlobs | Export-Csv -Path (Join-Path -Path $OutputPath -ChildPath "$OutputFile.csv") -Append -NoTypeInformation
                <#
                if($Clipboard.IsPresent){
                    ($PartitionsBlobs | Sort-Object -Property PartitionID | ConvertTo-Csv -NoTypeInformation).Replace(",","`t") | Set-Clipboard
                }
                #>

                return $PartitionsBlobs
            }

        }
        
        end {
            #Removing temporary files
            Remove-Item -Path $consumerTempFolder\* -Force
        }
    }

Set-Alias -Name PartRep Get-PartitionsReport
Export-ModuleMember -Function Get-PartitionsReport -Alias "PartRep"