functions/Invoke-ELCentralizeLogging.ps1

function Invoke-ELCentralizeLogging {
    <#
    .SYNOPSIS
        Copy function for centralizing log files out of exchange servers
 
    .DESCRIPTION
        This one is a utility function inside the ExchangeLogs module.
        It's intendet to create a centralized logging directory to gather logfiles (supported by the module) from all exchange servers into a single directory or fileshare and withing a folder structure eligible for further processing.
 
        The intented workflow provided by this function:
          - find a exchange server to connect on via active directory serviceconnectionpoitns
          - connect to exchange server
          - find all exchange servers and subsequently query logging path for supported services
          - copy the logfiles out of the exchange servers to a central logging share (usually a staging folder, due to further processing with Invoke-ELExchangeLogConvert. See more info in Invoke-ELExchangeLogConvert help.)
          - remembering last processed file, to reduce noise and load on next processing
 
        Requirements:
          - The executing user needs the permission to connect with powershell remoting into exchange server
                - Predefined roles like "Organization Management" or "Server Management" can be used
                - Find/ create a individual management role with Get-ManagementRole cmdlet
          - The executing user needs permission to access the administrative shares on the server
          - The users needs read/write permission on the central network share
 
        The workflow will be:
            - First, find all logs and centralize them into a staging-folder with 'Invoke-ELCentralizeLogging'
            - Second, process staging-folder with 'Invoke-ELExchangeLogConvert' and build read- and processable CSV files in a reporting-folder
            - Use other BI-/Analytical tools to process the CSV Files in the reporting folder
 
    .PARAMETER Destination
        The path to copy all logfiles from the exchange server(s) to.
        Can be a local directory, or a fileshare (recommended)
 
    .PARAMETER FilterServer
        Filter parameter to in-/exclude certain exchange server from processing.
        The filter is applied in a "like"-matching process, so wildcards and multiple values for inclusion are supportet.
 
        Default value: *
 
    .PARAMETER IncludeLog
        Filter parameter to specify the type of logs to query and process.
        This filter ofers a predefined set of values: "All", "HubTransport", "Frontendtransport", "IMAP4", "POP3"
 
        Multiple values are supported.
        Default value: All
 
    .PARAMETER DirectoryLastProcessedFile
        The folder where the function search for the information which file was processed as last file on the previous run.
        Should be a local directory, but can also be a share.
 
        Default value: C:\Administration\Logs\Exchange\CentralizedLogs
 
    .PARAMETER LogFile
        The logfile to archive processing information.
 
        Default value: -not specified-
        Recommended: C:\Administration\Logs\Exchange\CentralizeExchangeLogs.log
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Invoke-ELCentralizeLogging -Destination "\\SRV01\Logs\Exchange\Staging"
 
        Gathers all exchange servers with all supported logs and copy it to the share "\\SRV01\Logs\Exchange\Staging".
        No log is written, there are basic output information on the console and extended logging information in the session via "Get-PSFMessage".
 
    .EXAMPLE
        PS C:\> Invoke-ELCentralizeLogging -Destination "\\$($env:USERDNSDOMAIN)\system\Logs\Exchange\Staging" -LogFile "C:\Administration\Logs\CentralizedLogs\CentralizeExchangeLogs.log"
 
        Gathers all exchange servers with all supported logs and copy it to the share "\\$($env:USERDNSDOMAIN)\System\Logs\Exchange\Staging". This one assumes, there is a DFS share "System" in your domain with a folder "logs".
        Script actions are written to a logfile 'C:\Administration\Logs\CentralizedLogs\CentralizeExchangeLogs.log' for informationtracking of the process state. This one is usally recommended for usage in scheduled tasks/ automation.
 
        Additonally, there are basic output information on the console and extended logging information in the session via "Get-PSFMessage".
 
    .EXAMPLE
        PS C:\> Invoke-ELCentralizeLogging -Destination "\\SRV01\Logs\Exchange\Staging" -DirectoryLastProcessedFile "C:\Administration\Logs\CentralizedLogs" -LogFile "C:\Administration\Logs\CentralizedLogs\CentralizeExchangeLogs.log" -FilterServer "Ex01", "Ex02" -IncludeLog "HubTransport", "Frontendtransport"
 
        Gathers only exchange server "Ex01" and "Ex02" and only SMTP logs (Hub and Frontend transport service) and copy it to the share "\\SRV01\Logs\Exchange\Staging".
        Last processed files will be remembered in directory "C:\Administration\Logs\CentralizedLogs". This is the default used directory, but it can be changed to anything else.
        Script actions are written to a logfile 'C:\Administration\Logs\CentralizedLogs\CentralizeExchangeLogs.log' for informationtracking of the process state. This one is usally recommended for usage in scheduled tasks/ automation.
 
        Additonally, there are basic output information on the console and extended logging information in the session via "Get-PSFMessage".
 
    .EXAMPLE
        PS C:\> Invoke-ELCentralizeLogging -Path "\\$($env:USERDNSDOMAIN)\system\Logs\Exchange\Staging" -LastFileDirectory "C:\Administration\Logs\CentralizedLogs"
 
        Same result as in previous examples but with alias "Path" and "LastFileDirectory" on parameters "Destination" and "DirectoryLastProcessedFile".
#>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true)]
        [Alias("Path")]
        [String]
        $Destination,

        [string[]]
        $FilterServer = "*",

        [ValidateSet("All", "HubTransport", "Frontendtransport", "IMAP4", "POP3")]
        [string[]]
        $IncludeLog = "All",

        [Alias("LastFileDirectory")]
        [String]
        $DirectoryLastProcessedFile = "C:\Administration\Logs\Exchange\CentralizedLogs",

        [String]
        $LogFile
    )

    begin {}

    process {}

    end {
        #region Init and prerequisites
        if ($LogFile) { Initialize-LogFile -LogFile $LogFile -AlternateLogName $MyInvocation.MyCommand -LogInstanceName "ExchangeLogs" }
        Write-PSFMessage -Level Host -Message "----- Start script -----" -Tag "ExchangeLogs"

        # variables
        Write-PSFMessage -Level System -Message "Initialize variables" -Tag "ExchangeLogs"
        if ($DirectoryLastProcessedFile.EndsWith("\")) { $DirectoryLastProcessedFile = $DirectoryLastProcessedFile.TrimEnd("\") }
        if ($Destination.EndsWith("\")) { $Destination = $Destination.TrimEnd("\") }

        # find exchange server in active directory
        try {
            $adsiSearch = ([ADSISearcher]'(&(objectclass=serviceconnectionpoint)(|(keywords=77378F46-2C66-4aa9-A6A6-3E7A48B19596)(keywords="67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68")))')
            $adsiSearch.SearchRoot = [adsi]"LDAP://CN=Services,CN=Configuration,$(([adsi]::new()).distinguishedName)"
            $exServerName = ($adsiSearch.FindOne()).Properties['Name']
        } catch {
            Stop-PSFFunction -Message "Unable to query active directory for exchange serviceconnectionpoint (Message: $($_.exception.message))" -ErrorRecord $_ -Exception $_.exception -Tag "ExchangeLogs"
            throw
        }
        Write-PSFMessage -Level System -Message "Found exchange server: $($exServerName)" -Tag "ExchangeLogs"


        # check directories
        Write-PSFMessage -Level System -Message "Check directory to memorize last processed file ($($DirectoryLastProcessedFile))" -Tag "ExchangeLogs"
        if (-not (Test-Path -Path $DirectoryLastProcessedFile)) {
            try {
                $null = New-Item -Path $DirectoryLastProcessedFile -ItemType Directory -Force -ErrorAction Stop
            } catch {
                Stop-PSFFunction -Message "Error creating directory to memorize last processed file '$($DirectoryLastProcessedFile)' (Message: $($_.exception.message))" -ErrorRecord $_ -Exception $_.exception  -Tag "ExchangeLogs"
                throw
            }
        }

        Write-PSFMessage -Level System -Message "Check destination directory ($($Destination))" -Tag "ExchangeLogs"
        if (-not (Test-Path -Path $Destination)) {
            try {
                $null = New-Item -Path $Destination -ItemType Directory -Force -ErrorAction Stop
            } catch {
                Stop-PSFFunction -Message "Error creating destination directory '$($DirectoryLastProcessedFile)' (Message: $($_.exception.message))" -ErrorRecord $_ -Exception $_.exception -Tag "ExchangeLogs"
                throw
            }
        }


        # connect to exchange server
        Write-PSFMessage -Level Verbose -Message "Init exchange session" -Tag "ExchangeLogs"
        try {
            $exSession = New-PSSession -ConfigurationName Microsoft.Exchange -ErrorAction Stop -ConnectionUri "http://$($exServerName)/PowerShell/"
            $null = Import-PSSession $exSession -DisableNameChecking -ErrorAction Stop
        } catch {
            Stop-PSFFunction -Message "Error connecting exchange session '$($exServerName)' (Message: $($_.exception.message))" -ErrorRecord $_ -Exception $_.exception -Tag "ExchangeLogs"
            throw
        }
        Write-PSFMessage -Level Verbose -Message "Created $($exSession.count) sessions. Server:$($exServerName)" -Tag "ExchangeLogs"


        #endregion Init and prereqs



        #region Query Logdata from exchange
        # query log paths
        $exServer = Get-ExchangeServer
        $exServer = foreach ($filterItem in $FilterServer) {
            $exServer | Where-Object -Property Name -Like $filterItem
        }
        $exServer = $exServer | Sort-Object -Property Name -Unique
        Write-PSFMessage -Level Verbose -Message "Found $($exServer.count) exchange server" -Tag "ExchangeLogs"

        if("All" -in $IncludeLog -or "HubTransport" -in $IncludeLog) {
            $logPathSmtpHub = Get-TransportService | Sort-Object -Property Name | Select-Object -Property Name, SendProtocolLogPath, ReceiveProtocolLogPath
            Write-PSFMessage -Level Verbose -Message "Found $($logPathSmtpHub.count) TransportService" -Tag "ExchangeLogs"
        }

        if("All" -in $IncludeLog -or "Frontendtransport" -in $IncludeLog) {
            $logPathSmtpFrontEnd = Get-FrontEndTransportService | Sort-Object -Property Name | Select-Object -Property Name, SendProtocolLogPath, ReceiveProtocolLogPath
            Write-PSFMessage -Level Verbose -Message "Found $($logPathSmtpFrontEnd.count) FrontEndTransportService" -Tag "ExchangeLogs"
        }

        if("All" -in $IncludeLog -or "IMAP4" -in $IncludeLog) {
            $logPathImap = $exServer | ForEach-Object { Get-ImapSettings -Server $_.name } | Sort-Object -Property Server | Select-Object -Property Server, LogFileLocation
            Write-PSFMessage -Level Verbose -Message "Found $($logPathImap.count) ImapSettings" -Tag "ExchangeLogs"
        }

        if("All" -in $IncludeLog -or "POP3" -in $IncludeLog) {
            $logPathPop = $exServer | ForEach-Object { Get-PopSettings -Server $_.name } | Sort-Object -Property Server | Select-Object -Property Server, LogFileLocation
            Write-PSFMessage -Level Verbose -Message "Found $($logPathPop.count) PopSettings" -Tag "ExchangeLogs"
        }


        # build server/path list
        $sourcePaths = @()
        # SMTP Hub Transportservice
        foreach ($item in $logPathSmtpHub) {
            $sourcePaths += [PSCustomObject]@{
                "ComputerName" = $item.Name
                "Path"         = $item.SendProtocolLogPath
                "Type"         = "Transport-Hub"
            }
            $sourcePaths += [PSCustomObject]@{
                "ComputerName" = $item.Name
                "Path"         = $item.ReceiveProtocolLogPath
                "Type"         = "Transport-Hub"
            }
        }

        # SMTP Frontend Transportservice
        foreach ($item in $logPathSmtpFrontEnd) {
            $sourcePaths += [PSCustomObject]@{
                "ComputerName" = $item.Name
                "Path"         = $item.SendProtocolLogPath
                "Type"         = "Transport-FrontEnd"
            }
            $sourcePaths += [PSCustomObject]@{
                "ComputerName" = $item.Name
                "Path"         = $item.ReceiveProtocolLogPath
                "Type"         = "Transport-FrontEnd"
            }
        }

        # IMAP service
        foreach ($item in $logPathImap) {
            $sourcePaths += [PSCustomObject]@{
                "ComputerName" = $item.Server
                "Path"         = $item.LogFileLocation
                "Type"         = "ClientAccess"
            }
        }

        # POP3 service
        foreach ($item in $logPathPop) {
            $sourcePaths += [PSCustomObject]@{
                "ComputerName" = $item.Server
                "Path"         = $item.LogFileLocation
                "Type"         = "ClientAccess"
            }
        }
        $sourcePaths = $sourcePaths | Sort-Object ComputerName, Path
        Write-PSFMessage -Level Verbose -Message "$($sourcePaths.count) paths to process." -Tag "ExchangeLogs"


        #endregion Query Logdata from exchange



        #region process logfiles
        Write-PSFMessage -Level Host -Message "Start working through each server and each directory" -Tag "ExchangeLogs"


        # work through each server and each directory
        foreach ($server in $exServer.name) {
            Write-PSFMessage -Level Verbose -Message "Working with $($server)" | Out-File -FilePath $LogFile -Encoding default -Force -Append
            $sourcePathInServer = $sourcePaths | Where-Object ComputerName -like $server

            foreach ($source in $sourcePathInServer) {
                # variables for directory
                $counter = 0
                $processedFiles = @()
                $lastFile = "$($DirectoryLastProcessedFile)\Last-$($source.Type)-$($source.Path.split("\")[-1])-$($server).xml"
                $destinationPath = "$($Destination)\$($source.Type)\$($source.Path.split("\")[-1])\$($server)"

                Write-PSFMessage -Level Verbose -Message "Processing: $($source) --to--> '$destinationPath'" -Tag "ExchangeLogs"

                # test and create destination directory
                Write-PSFMessage -Level VeryVerbose -Message "Check destination path '$($destinationPath)'" -Tag "ExchangeLogs"
                if (-not (Test-Path -Path $destinationPath)) {
                    try {
                        $null = New-Item -Path $destinationPath -ItemType Directory -Force
                    } catch {
                        Stop-PSFFunction -Message "Unable to create directory '$($destinationPath)' (Message: $($_.Exception.Message))" -ErrorRecord $_ -Exception $_.exception -Tag "ExchangeLogs"
                        throw
                    }
                }

                # gathering files
                $sourcePath = "\\$($server)\$($source.path.Replace(":","$"))"
                $sourceFiles = Get-ChildItem -Path $sourcePath | Sort-Object -Property Name
                Write-PSFMessage -Level VeryVerbose -Message "There are $($sourceFiles.count) files in $($source)" -Tag "ExchangeLogs"

                # enumerate files in source to getting an filterable order
                foreach ($file in $sourceFiles) {
                    $file | Add-Member -NotePropertyName "Order" -NotePropertyValue $counter -Force
                    $counter++
                }

                # if exist, query last processed file
                if (Test-Path -Path $LastFile) { $lastFileProcessed = Import-PSFClixml -Path $LastFile } else { $lastFileProcessed = $null }
                if ($lastFileProcessed) {
                    $sourceFileStartFilter = $sourceFiles | Where-Object Name -like $lastFileProcessed.Name
                    $sourceFiles = $sourceFiles | Where-Object order -gt $sourceFileStartFilter.Order
                }

                # ignore current logfile
                $sourceFiles = $sourceFiles[0 .. ($sourceFiles.count - 2)]
                Write-PSFMessage -Level VeryVerbose -Message "$($sourceFiles.count) files to process" -Tag "ExchangeLogs"


                # copy files to destination
                foreach ($file in $sourceFiles) {
                    Write-PSFMessage -Level Debug -Message "Copy $($file.Name)" -Tag "ExchangeLogs"
                    try {
                        if ($pscmdlet.ShouldProcess("$($file.FullName) to $($destinationPath)", "Copy")) {
                            Copy-Item -Path $file.FullName -Destination $destinationPath -ErrorAction Stop
                        }
                        $processedFiles += $file
                    } catch {
                        Stop-PSFFunction -Message "Unable to copy file $($file.name) to $($destinationPath). (Message: $($_.Exception.Message))" -ErrorRecord $_ -Exception $_.exception -Tag "ExchangeLogs"
                        throw
                    }
                }


                # export lastprocessed file for next run
                if ($processedFiles) {
                    Write-PSFMessage -Level Host -Message "Processed $($processedFiles.count) files and remembering LastFileProcessed: $($processedFiles[-1].FullName)" -Tag "ExchangeLogs"
                    if ($pscmdlet.ShouldProcess("Last processed file '$($processedFiles[-1].FullName)' to $($lastFile)", "Export")) {
                        $processedFiles[-1] | Export-PSFClixml -Path $lastFile -Force
                    }
                } else {
                    Write-PSFMessage -Level Host -Message "No files processed from directory: $($sourcePath)" -Tag "ExchangeLogs"
                }
            }
        }
        #endregion process logfiles



        #region cleanup
        Write-PSFMessage -Level VeryVerbose -Message "Closing exchange session" -Tag "ExchangeLogs"
        $exSession | Remove-PSSession

        Write-PSFMessage -Level Host -Message "*** Finishing script ***" -Tag "ExchangeLogs"
        #endregion cleanup
    }
}