#StackBounty: #console #logging #powershell Advanced Logging Function

Bounty: 100

Way more blood, sweat, and tears have went into this then I would care to admit, but this has been a function I have been using in some shape or form for quite a while and it has taught me alot.

It does exactly what I want it to do ATM but happy to hear any suggestions on what could make it run more efficent. I am looking at making a buffer queue for the streamwriter to improve write performance but for for scripts is doesn’t really seem necessary, more just of a fun little addition to add.

Will most likely add the ability to write to event log next, but again there is no real need for it at this time.

function Write-Tee {
    Param(
        [Parameter(Mandatory=$False)]
        [Switch]$Close,
        [Parameter(Mandatory=$False)]
        [ValidateNotNullorEmpty()]
        [String]$Message,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Fatal','Error','Warning','Info','Trace')]
        [String]$Severity,
        [Parameter(Mandatory=$False)]
        [Switch]$WriteToHost=$True,
        [Parameter(Mandatory=$False)]
        [Switch]$WriteToLog=$True,
        [Parameter(Mandatory=$False)]
        [Switch]$FirstRun=$False
    )
    Begin {
        $Logtime = [datetime]::Now
        #Function specific settings used for customization.
        [Int]$LoggingLevel  = 3
        [Bool]$WriteLog     = $True
        [Bool]$WriteConsole = $True
        [Int]$LogBuffer     = 25
        [String]$LogPath    = ''
        [String]$LogName    = ''

        Set-StrictMode -Version Latest
        $ErrorActionPreference = 'Stop'

        #Make sure previous streamwriters are closed out prior to starting script.
        If((test-path variable:WriteTee) -and $FirstRun) {
            Write-Tee -Close
        } ElseIf (!(test-path variable:WriteTee) -OR $FirstRun) {
            New-Variable -ErrorAction SilentlyContinue -Force -Name WriteTee -Value @{} -Scope Script
            $Script:WriteTee.RunOnce = $True
        }

        If ($WriteTee.RunOnce) {
            #Set important functional variables
            $Script:WriteTee.RunOnce = $False
            $Script:WriteTee.Computer = $env:COMPUTERNAME
            $Script:WriteTee.SID = [Security.Principal.WindowsIdentity]::GetCurrent().Name
            If ($Host.name -eq "Windows PowerShell ISE Host") {
                $Script:WriteTee.ForegroundColor = 'White'
                $Script:WriteTee.BackgroundColor = 'DarkMagenta'
            } Else {
                $Script:WriteTee.Host = (get-host).ui.rawui
                $Script:WriteTee.ForegroundColor = $Script:WriteTee.Host.ForegroundColor
                $Script:WriteTee.BackgroundColor = $Script:WriteTee.Host.BackgroundColor
            }

            #Dynamicly pull any script defined variables.
            If (Test-Path Variable:Script) {
                If ($Script.ContainsKey("WriteTee")) {
                    ForEach ($Key in $Script.WriteTee.GetEnumerator()) {
                        $Script:WriteTee.$($Key.Name) = $Key.Value
                    }
                }
            }

            #Dynamicly Convert REQUIRED funciton config to variables if not already defined.
            ForEach ($Var in ("LogLevel","WriteLog","WriteConsole","LogBuffer","LogPath","LogName")) {
                If (!$WriteTee.ContainsKey("$Var")) {
                    $WriteTee.Temp = Get-Variable -Name $Var -ValueOnly -ErrorAction SilentlyContinue
                    IF ($WriteTee.Temp -OR $Var -IN ("LogPath","LogName")) {
                        $WriteTee.$Var = $WriteTee.Temp
                    } Else {
                        Throw "$Var is not defined in either `$Script nor in the function config."
                    }
                }
                $WriteTee.Remove('Temp')
            }

            #Ensures there is a valid log level set.
            If ($WriteTee.LogLevel -LT 1 -OR $WriteTee.LogLevel -GT 5) {
                Write-Error "`$LogLevel does not contain a value between 1 and 5"
            }


            If ($WriteTee.WriteLog) {


                    $WriteTee.LogPaths = $WriteTee.LogPath,
                         $(split-path -parent $Script:MyInvocation.MyCommand.Definition),
                         $Env:UserProfile
                    #Verify that the logname is in a usable format.
                    Try {
                        $WriteTee.LogName = [System.IO.FileInfo]$WriteTee.LogName
                    } Catch {
                        $WriteTee.LogName = "$(([System.IO.FileInfo](split-path -leaf $Script:MyInvocation.MyCommand.Definition)).BaseName).log"
                    }

                    #Find the first usable log file.
                    Foreach ($Path in $WriteTee.LogPaths) {
                        If ($WriteTee.ContainsKey('LogFile')) {
                            Continue
                        } Else {
                            Try {
                                $Path = [System.IO.DirectoryInfo]$Path
                            } Catch {
                                #If you can't convert it, script can't use it.
                                Continue
                            }
                        }

                        #Ensure the directory exists and if not, create it.
                        If (![System.IO.Directory]::Exists($Path)) {
                            Try {
                                #Create the directory because .Net will error out if you try to create a file in a directory that doesn't exist yet.
                                New-Item -Path $Path.Parent.FullName -Name $Path.BaseName -ItemType 'Directory' -ErrorAction Stop -Force |Out-Null
                            } Catch {
                                Continue
                            }
                        }#End-If

                        #Check to see if there are any existing logs
                        $WriteTee.ExistingLogs = @(Get-ChildItem -LiteralPath $Path.FullName -Filter "$(([System.IO.FileInfo]$WriteTee.LogName).BaseName)*$(([System.IO.FileInfo]$WriteTee.LogName).Extension)" |Sort-Object)
                        If ($WriteTee.ExistingLogs.Count -GT 0) {
                            ForEach ($ExistingLog in $WriteTee.ExistingLogs) {
                                Try {
                                    [io.file]::OpenWrite($ExistingLog.FullName).close() |Out-Null
                                    $Script:WriteTee.LogFile = $ExistingLog.FullName
                                    break
                                } Catch {
                                    $WriteTee.LastLogName = $ExistingLog
                                    Continue
                                }
                            }
                            If ($Script:WriteTee.ContainsKey('LogFile')) {Break}
                        }#End-If

                        #If no previous logs can be added to create a new one.
                        switch ($WriteTee.ExistingLogs.Count) {
                            {$PSItem -EQ 0} {
                                $WriteTee.TestLogFile = Join-Path -Path $Path -ChildPath $WriteTee.LogName
                            }
                            {$PSItem -EQ 1} {
                                $WriteTee.TestLogFile = Join-Path -Path $Path -ChildPath ($WriteTee.LastLogName.basename + '[0]' + $WriteTee.LastLogName.Extension)
                            }

                            {$PSItem -GE 2} {
                                $WriteTee.TestLogFile = $WriteTee.LastLogName.FullName.Split('[]')
                                $WriteTee.TestLogFile = ($WriteTee.TestLogFile[0] + '[' + (([int]$WriteTee.TestLogFile[1]) + 1) + ']' + $WriteTee.TestLogFile[2])
                            }
                            Default {
                                Write-Host "If you are looking for an explanation of how you got here, I can tell you I don't have one. But what I do have are a very particular lack of answers that I have aquired over a very long career that make these problems a nightmare for people like me."
                                Continue
                                }
                        }#End-Switch

                        #Last but not least, try to create the file and hope it is successful.
                        Try {
                            [IO.File]::Create($WriteTee.TestLogFile, 1, 'None').close() |Out-Null
                            $Script:WriteTee.LogFile = $WriteTee.TestLogFile
                            $WriteTee.NewFile = $True
                            Break
                        } Catch {
                            Continue
                        }
                    }#End-ForEach

                #Rev up the StreamWriter
                $Script:WriteTee.StreamWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList "$($Script:WriteTee.LogFile)",$True,$([System.Text.Encoding]::Unicode)
                If ($WriteTee.ContainsKey('NewFile')) {
                    $Script:WriteTee.StreamWriter.WriteLine("Computer Name`tSecurity Principal`tDate`tTime`tScript Phase`tLine Number`tMessage")
                }
            }#EndIf
        }#EndIf

        #Convert easily readable severity to INT to determine log output level.


        Switch ($Severity) {
            'Trace'   {$WriteTee.ErrorLevel = '5'; $WriteTee.FGColor=$Script:WriteTee.ForegroundColor; $WriteTee.BGColor = $Script:WriteTee.BackgroundColor;}
            'Info'    {$WriteTee.ErrorLevel = '4'; $WriteTee.FGColor=$Script:WriteTee.ForegroundColor; $WriteTee.BGColor = $Script:WriteTee.BackgroundColor;}
            'Warning' {$WriteTee.ErrorLevel = '3'; $WriteTee.FGColor='Yellow'; $WriteTee.BGColor = 'Black';}
            'Error'   {$WriteTee.ErrorLevel = '2'; $WriteTee.FGColor='Red'; $WriteTee.BGColor = 'Black';}
            'Fatal'   {$WriteTee.ErrorLevel = '1'; $WriteTee.FGColor='Red'; $WriteTee.BGColor = 'Black';}
            Default   {$WriteTee.ErrorLevel = '0'}
        }#EndSwitch

        #If the script doesn't meet the minimum log level, disable output running.
        If ($WriteTee.ErrorLevel -GT $WriteTee.LogLevel) { 
            $WriteTee.Skip = $True
        } ElseIF($WriteTee.ErrorLevel -EQ 0 -AND !$Message) {
            $WriteTee.Skip = $True
            #Write-Error -message "Severity level must be defined"
        } Else {
            #Convert Date Time to more usable format.
            $WriteTee.Skip = $False
            $WriteTee.Time = $Logtime.ToString('HH:mm:ss,fff')
            $WriteTee.Date = $Logtime.ToString('MM-dd-yyyy')
            $WriteTee.Line = $MyInvocation.ScriptLineNumber
        }

    }#End-Begin

    Process {
        If ($WriteTee.Skip -EQ $False) {
            IF ($WriteTee.WriteConsole) {
                $WriteTee.OutConsole = "$($WriteTee.Date) $($WriteTee.Time) - $($Script.Phase) - $($WriteTee.Line) - $Message"
                Write-Host -Object $WriteTee.OutConsole -ForegroundColor $WriteTee.FGColor -BackgroundColor $WriteTee.BGColor
            }
            ## Write output to log file.
            IF ($WriteTee.WriteLog) {
                $WriteTee.OutLog = "$($Script:WriteTee.Computer)`t$($Script:WriteTee.SID)`t$($WriteTee.Date)`t$($WriteTee.Time)`t$($Script.Phase)`t$($WriteTee.Line)`t$Message"
                $Script:WriteTee.StreamWriter.WriteLine($WriteTee.OutLog)
            }
        }
    }#End-Process
    End {
        If ($Close -AND $Script:WriteTee) {
            $Script:WriteTee.StreamWriter.Flush()
            $Script:WriteTee.StreamWriter.Close()
            Remove-Variable -ErrorAction SilentlyContinue -Force -Name WriteTee -Scope Script
        }#End-If
    }#End-End

}
clear
$Script = @{}
$Script.WriteTee = @{}
$Script.WriteTee.LogLevel = 5
$Script.Phase = "Debug"

Write-Tee -Message "This is AWESOME!" -Severity Info
Write-Tee -Close


Get this bounty!!!

Leave a Reply