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 {
    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."

            #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),
                    #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')) {
                        } Else {
                            Try {
                                $Path = [System.IO.DirectoryInfo]$Path
                            } Catch {
                                #If you can't convert it, script can't use it.

                        #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 {

                        #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
                                } Catch {
                                    $WriteTee.LastLogName = $ExistingLog
                            If ($Script:WriteTee.ContainsKey('LogFile')) {Break}

                        #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."

                        #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
                        } Catch {

                #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")

        #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'}

        #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


    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"
    End {
        If ($Close -AND $Script:WriteTee) {
            Remove-Variable -ErrorAction SilentlyContinue -Force -Name WriteTee -Scope Script

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

Write-Tee -Message "This is AWESOME!" -Severity Info
Write-Tee -Close
  • \$\begingroup\$ try to see at Logging module. it use a separate thread for logging process. \$\endgroup\$
    – mazzy
    Commented May 23, 2018 at 9:57


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.