ShoutOut.psm1

if ( !(Get-variable "_ShoutOutSettings" -ErrorAction SilentlyContinue) -or $script:_ShoutOutSettings -isnot [hashtable]) {
    $script:_ShoutOutSettings = @{
        DefaultMsgType="Info"
        DefaultLog="C:\temp\shoutOut.{0}.{1}.{2:yyyyMMddHHmmss}.log" -f $env:COMPUTERNAME, $pid, [datetime]::Now
        LogFileRedirection=@{}
        MsgStyles=@{
            Success =       @{ ForegroundColor="Green" }
            Exception =     @{ ForegroundColor="Red"; BackgroundColor="Black" }
            Error =         @{ ForegroundColor="Red" }
            Warning =       @{ ForegroundColor="Yellow"; BackgroundColor="Black" }
            Info =          @{ ForegroundColor="Cyan" }
            Result =        @{ ForegroundColor="White" }
        }
        LogContext=$true
        Disabled=$false
    }
}
function _ensureShoutOutLogFile {
    param(
        [string]$logFile,
        [string]$MsgType = "*"
    )

    if (!(Test-Path $logFile -PathType Leaf)) {
        try {
            return new-Item $logFile -ItemType File -Force -ErrorAction Stop
        } catch {
            "Unable to create log file '{0}' for '{1}'." -f $logFile, $msgType | shoutOut -MsgType Error
            "Messages marked with '{0}' will be redirected." -f $msgType | shoutOut -MsgType Error
            shoutOut $_ Error
            throw ("Unable to use log file '{0}', the file cannot be created." -f $logFile)
        }
    }

    return gi $logFile
}
function _ensureshoutOutLogHandler {
    param(
        [scriptblock]$logHandler,
        [string]$msgType = "*"
    )

    $params = $logHandler.Ast.ParamBlock.Parameters

    if ($params.count -eq 0) {
        "Invalid handler, no parameters found: {0}" -f $logHandler | shoutOut -MsgType Error
        "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error
        throw "No parameters declared by the given handler."
    }

    $paramName = '$message'
    $param = $params | ? { $_.Name.Extent.Text -eq $paramName }

    if (!$param) {
        "Invalid handler, no '{0}' parameter found" -f $paramName | shoutOut -MsgType Error
        "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error
        throw ("No '{0}' parameter declared by the given handler." -f $paramName)
    }

    if (($t = $param.StaticType) -and !($t.IsAssignableFrom([String])) ) {
        "Invalid handler, the '{0}' parameter should accept values of type [String]." -f $paramName | shoutOut -MsgType Error
        "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error
        throw ("'{0}' parameter on the given handler is of invalid type (not assignable from [string])." -f $paramName)
    }

    return $logHandler
}

function Clear-ShoutOutRedirect {
    param(
        [Parameter(HelpMessage="Mesage Type to remove redirection of.")]
        [string]$msgType
    )

    if ($_ShoutOutSettings.LogFileRedirection.ContainsKey($msgType)) {
        $l = $_ShoutOutSettings.LogFileRedirection[$msgType]
        "Removing message redirection for '{0}', messages of this type will be logged in the default log file ('{1}')." -f $msgType, $_ShoutOutSettings.DefaultLog | shoutOut -LogFile $l
        $_ShoutOutSettings.LogFileRedirection.Remove($msgType)
        "Removed message redirection for '{0}', previously messages were written to '{1}'." -f $msgType, $l | shoutOut
    }
}
function Get-ShoutOutConfig {
  return $_ShoutOutSettings
}
function Get-ShoutOutDefaultLog {
    param()

    return $script:_ShoutOutSettings.DefaultLog
}

function Get-ShoutOutRedirect {
    param(
        [Parameter(HelpMessage="Message Type to retrieve redirection information for.")]
        [string]$msgType
    )

    return $script:_ShoutOutSettings.LogFileRedirection[$msgType]
} 
function Set-ShoutOutConfig {
    param(
        [Parameter(HelpMessage="The default Message Type that ShoutOut should apply to messages.")]
        [string]$DefaultMsgType,
        [Parameter(HelpMessage="Default log handler to use for Messages Types without redirection.")]
        [Alias("LogFile")]
        $Log,
        [Parameter(HelpMessage="Enable/Disable Context logging.")]
        [Alias("LogContext")]
        [boolean]$EnableContextLogging,
        [Parameter(HelpMessage="Disable/Enable ShoutOut.")]
        [Alias("Disabled")]
        [boolean]$DisableLogging
    )

    if ($PSBoundParameters.ContainsKey("DefaultMsgType")) {
        $_shoutOutSettings.DefaultMsgType = $DefaultMsgType
    }

    if ($PSBoundParameters.ContainsKey("Log")) {
        Set-ShoutOutDefaultLog $Log | Out-Null
    }

    if ($PSBoundParameters.ContainsKey("LogContext")) {
        $_shoutOutSettings.LogContext = $LogContext
    }
    
    if ($PSBoundParameters.ContainsKey("Disabled")) {
        $_shoutOutSettings.Disabled = $Disabled
    }

}
function Set-ShoutOutDefaultLog {
    param(
        [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogFilePath")][String]$LogFilePath,
        [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogFile")][System.IO.FileInfo]$LogFile,
        [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogHandler")][scriptblock]$LogHandler
    )

    switch ($PSCmdlet.ParameterSetName) {
        "LogFilePath" {
            try {
                _ensureShoutOutLogFile $LogFilePath -ErrorAction Stop | Out-Null
                $_shoutOutSettings.DefaultLog = $LogFilePath
            } catch {
                return $_
            }
        }
        "LogFile" {
            try {
                _ensureShoutOutLogFile $LogFile.FullName -ErrorAction Stop | Out-Null
                $_shoutOutSettings.DefaultLog = $LogFile.FullName
            } catch {
                return $_
            }
        }
        "LogHandler" {
            try {
                _ensureshoutOutLogHandler $LogHandler -ErrorAction Stop | Out-Null
                $_shoutOutSettings.DefaultLog = $LogHandler
            } catch {
                return $_
            }
        }
    }
    
}
function Set-ShoutOutRedirect {
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true, Position=1, HelpMessage="Message type to redirect.")][string]$MsgType,
        [Parameter(ParameterSetName="FilePath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")][string]$LogFilePath,
        [Parameter(ParameterSetName="FileInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="FileInfo object.")][System.IO.FileInfo]$LogFile,
        [Parameter(ParameterSetName="Scriptblock", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="ScriptBlock to use as log handler.")][scriptblock]$LogHandler
    )


    $log = $null
    switch ($PSCmdlet.ParameterSetName) {
    "Scriptblock" {
            try {
                _ensureshoutOutLogHandler $LogHandler $msgType | Out-Null
            } catch {
                return $_
            }
            $log = $LogHandler
            break
        }
    "FileInfo" {
            try {
                _ensureShoutOutLogFile $LogFile.FullName $msgType | Out-Null
                $_shoutOutSettings.DefaultLog = $LogFile.FullName
            } catch {
                return $_
            }

            $log = $LogFile.FullName
        }
    "FilePath" {
            try {
                _ensureShoutOutLogFile $LogFilePath $msgType | Out-Null
            } catch {
                return $_
            }

            $log = $LogFilePath
        }
    }

    $oldLog = $_ShoutOutSettings.DefaultLog
    if ($_ShoutOutSettings.LogFileRedirection.ContainsKey($msgType)) {
        $oldLog = $_ShoutOutSettings.LogFileRedirection[$msgType]
    }
    "Redirecting messages of type '{0}' to '{1}'." -f $msgType, $log | shoutOut -MsgType Info
    $_ShoutOutSettings.LogFileRedirection[$msgType] = $log
    "Messages of type '{0}' have been redirected to '{1}'." -f $msgType, $log | shoutOut -MsgType Info
    "Previous log: '{1}'." -f $msgType, $oldLog | shoutOut -MsgType Info
}
# ShoutOut.ps1

# First-things first: Logging function (is the realest, push the message and let the harddrive feel it.)

<#
.SYNOPSIS
Pushes a message of the given $MsgType to the given $Log file with attached invocation metadata.
.DESCRIPTION
Logging function, used to push a message to a corresponding log-file.
The message is prepended with meta data about the invocation to shoutOut as:
<MessageType>|<Computer name>|<PID>|<calling Context>|<Date & time>|$Message
 
The default values for the parameters can be set using the Set-ShoutOutConfig,
Set-ShoutOutDefaultLog, and Set-ShotOutRedirect functions.
 
#>

function shoutOut {
    param(
        [parameter(Mandatory=$false,  position=1, ValueFromPipeline=$true)] [Object]$Message,
        [Alias("ForegroundColor")]
        [parameter(Mandatory=$false, position=2)][String]$MsgType=$null,
        [parameter(Mandatory=$false, position=3)]$Log=$null,
        [parameter(Mandatory=$false, position=4)][Int32]$ContextLevel=1, # The number of levels to proceed up the call
                                                                         # stack when reporting the calling script.
        [parameter(Mandatory=$false)] [bool] $LogContext = (
            !$_ShoutOutSettings.ContainsKey("LogContext") -or ($_ShoutOutSettings.ContainsKey("LogContext") -and $_ShoutOutSettings.LogContext)
        ),
        [parameter(Mandatory=$false)] [Switch] $NoNewline,
        [parameter(Mandatory=$false)] [Switch] $Quiet
    )
    
    process {
        $defaultLogHandler = { param($msg) $msg | Out-File $Log -Encoding utf8 -Append }

        $msgObjectType = if ($null -ne $Message) {
            $Message.GetType()
        } else {
            $null
        }

        $msgObjectTypeName = if ($null -ne $msgObjectType) {
            $msgObjectType.Name
        } else {
            "NULL"
        }

        
        if ( (-not $PSBoundParameters.ContainsKey("MsgType")) -or ($null -eq $PSBoundParameters["MsgType"]) ) {
            
            switch ($msgObjectTypeName) {

                "ErrorRecord" {
                    $MsgType = "Error"
                }

                default {
                    if ([System.Exception].IsAssignableFrom($msgObjectType)) {
                        $MsgType = "Exception"
                    } else {
                        $MsgType = "Info"
                    }
                }
            }
        }

        # Apply global settings.
        if ( ( $settingsV = Get-Variable "_ShoutOutSettings" ) -and ($settingsV.Value -is [hashtable]) ) {
            $settings = $settingsV.Value

            if ($settings.ContainsKey("Disabled") -and ($settings.Disabled)) {
                Write-Debug "Call to Shoutout, but Shoutout is disabled. Turn back on with 'Set-ShoutOutConfig -Disabled `$false'."
                return
            }

            if (!$MsgType -and $settings.containsKey("DefaultMsgType")) { $MsgType = $settings.DefaultMsgType }
            if (!$Log -and $settings.containsKey("DefaultLog")) { $Log = $settings.DefaultLog }
            if ($settings.LogFileRedirection.ContainsKey($MsgType)) { $Log = $settings.LogFileRedirection[$MsgType] }
            
            if ($settings.containsKey("MsgStyles") -and ($settings.MsgStyles -is [hashtable]) -and $settings.MsgStyles.containsKey($MsgType)) {
                $msgStyle = $settings.MsgStyles[$MsgType]
            }
        }

        # Hard-coded defaults just in case.
        if (!$Log) { $Log = ".\setup.log" }
        
        if (!$msgStyle) {
            if ($MsgType -in [enum]::GetNames([System.ConsoleColor])) {
                $msgStyle = @{ ForegroundColor=$MsgType }
            } else {
                $msgStyle = @{ ForegroundColor="White" }
            }
        }
        
        # Apply formatting to make output more readable.
        switch ($msgObjectTypeName) {

            "String" {
                # No transformation necessary.
            }

            "NULL" {
                # No Transformation necessary.
            }

            "ErrorRecord" {
                if ($null -ne $message.Exception) {
                    shoutOut $message.Exception
                }

                if ($null -ne $message.InnerException) {
                    shoutOut $message.InnerException
                }

                $m = $message
                $Message = $m.Exception, $m.CategoryInfo, $m.InvocationInfo, $m.ScriptStackTrace | Out-string | % Split "`n`r" | ? { $_ }
                $Message = $Message | Out-String | % TrimEnd "`n`r"
            }

            default {
                $message = $Message | Out-String | % TrimEnd "`n`r"
            }
        }

        # Print to console if necessary
        if ([Environment]::UserInteractive -and !$Quiet) {
            $p = @{
                Object = $Message
                NoNewline = $NoNewline
            }
            if ($msgStyle.ForegroundColor) { $p.ForegroundColor = $msgStyle.ForegroundColor }
            if ($msgStyle.BAckgroundColor) { $p.BackgroundColor = $msgStyle.BackgroundColor }

            Write-Host @p
        }
        
        $parentContext = if ($LogContext) {
            $cs = Get-PSCallStack
            $csd = @($cs).Length
            # CallStack Depth, should always be greater than or equal to 2. 1 would indicate that we
            # are running the directly on the command line, but since we are inside the shoutOut
            # function there should always be at least one level to the callstack in addition to the
            # calling context.
            switch ($csd) {
                2 { "[{0}]<commandline>" -f $csd }
                
                default {
                    $parentCall = $cs[$ContextLevel]
                    if ($parentCall.ScriptName) {
                        "[{0}]{1}:{2}" -f $csd, $parentCall.ScriptName,$parentCall.ScriptLineNumber
                    } else {
                        for($i = $ContextLevel; $i -lt $cs.Length; $i++) {
                            $level = $cs[$i]
                            if ($level.ScriptName) {
                                break;
                            }
                        }

                        if ($level.ScriptName) {
                            "[{0}]{1}:{2}\<scriptblock>" -f $csd, $level.ScriptName,$level.ScriptLineNumber
                        } else {
                            "[{0}]<commandline>\<scriptblock>" -f $csd
                        }
                    }
                }
            }
        } else {
            "[context logging disabled]"
        }

        $createRecord = {
            param($m)
            "{0}|{1}|{2}|{3}|{4:yyyyMMdd-HH:mm:ss}|{5}|{6}" -f $MsgType, $env:COMPUTERNAME, $pid, $parentContext, [datetime]::Now, $msgObjectTypeName, $m
        }

        $record = . $createRecord $Message

        if ($log -is [scriptblock])  {
            try {
                . $Log -Message $record
            } catch {
                $errorMsgRecord1 = . $createRecord ("An error occurred while trying to log a message to '{0}'" -f ( $Log | Out-String))
                $errorMsgRecord2 = . $createRecord "The following is the record that would have been written:"
                $Log = "{0}\shoutOut.error.{1}.{2}.{3:yyyyMMddHHmmss}.log" -f $env:APPDATA, $env:COMPUTERNAME, $pid, [datetime]::Now
                $errorRecord = . $createRecord ($_ | Out-String)
                . $defaultLogHandler $errorMsgRecord1
                . $defaultLogHandler $errorRecord
                . $defaultLogHandler $errorMsgRecord2
                . $defaultLogHandler $record
            }
        } else {
            . $defaultLogHandler $record
        }
        
    }
}