Pslogg.psm1

<#
.SYNOPSIS
Functions for logging messages to the host or to PowerShell streams and, optionally, to a log file.
 
.DESCRIPTION
A module for logging messages to the host or to PowerShell streams, such as the Error stream or
the Information stream. In addition, messages may optionally be logged to a log file.
 
Messages are logged using the Write-LogMessage function.
 
The logger may be configured prior to logging any messages via function Set-LogConfiguration.
For example, the logger may be configured to write to the PowerShell host, or to PowerShell
streams such as the Error stream or the Verbose stream.
 
Function Get-LogConfiguration will return a copy of the current logger configuration as a hash
table. The configuration can be reset back to its default values via function
Reset-LogConfiguration.
 
.NOTES
 
#>


. $PSScriptRoot\Configuration.ps1

# Function naming conventions:
# ----------------------------
# Functions to be exported: Follow the standard PowerShell naming convention of
# "<verb>-<singular noun>", eg "Write-LogMessage".
#
# Private functions that
# shouldn't be exported: Have a "Private_" prefix. They must not include a dash, "-".
#
# These naming conventions simplify the exporting of public functions but not private ones
# because we can then simply export all functions containing a dash, "-".


#region Write Log Messages ************************************************************************

<#
.SYNOPSIS
Writes a message to the PowerShell host or a PowerShell stream and, optionally, to a log file.
 
.DESCRIPTION
Writes a message to either the PowerShell host or to a PowerShell stream, such as the Information
stream or the Verbose stream, depending on the logging configuration. In addition, the message may
be written to a log file, once again depending on the logging configuration.
 
.NOTES
The Pslogg logger can be configured via function Set-LogConfiguration with settings that persist
between messages. For example, it can be configured to write to the PowerShell host, or to
PowerShell streams such as the Error stream or the Verbose stream.
 
The most important configuration setting is the LogLevel, the session logging level. This
determines which messages will be logged and which will not.
 
Possible LogLevels, in order from highest to lowest, are:
    VERBOSE
    DEBUG
    INFORMATION
    WARNING
    ERROR
    OFF
         
Each message to be logged has a Message Level. This may be set explicitly when calling
Write-LogMessage or the default value of INFORMATION may be used. The Message Level is compared
to the LogLevel in the logger configuration. Only messages with a Message Level the same as or
lower than the configured LogLevel will be logged.
 
For example, if the LogLevel is INFORMATION then only messages with a Message Level of
INFORMATION, WARNING or ERROR will be logged. Messages with a Message Level of DEBUG or
VERBOSE will not be logged, as those levels are higher than INFORMATION.
 
When calling Write-LogMessage the Message Level can be set in two different ways:
 
    1) Via parameter -MessageLevel: The Message Level is specified as text. For example:
 
        Write-LogMessage 'Hello world' -MessageLevel 'VERBOSE'
 
    2) Via Message Level switch parameters: There are switch parameters for each possible
        Message Level: -IsVerbose, -IsDebug, -IsInformation, -IsWarning and -IsError. For
        example:
 
        Write-LogMessage 'Hello world' -IsVerbose
 
        Only one Message Level switch may be set for a given message.
 
Several configuration settings can be overridden for a single log message. The changes apply
only to that one message; subsequent messages will return to using the settings in the logger
configuration. Settings that can be overridden on a per-message basis are:
 
    1) The message destination: The message can be logged to a different destination from the
        one specified in the logger configuration by using the switch parameters -WriteToHost or
        -WriteToStreams;
 
    2) The host text color: If the message is being written to the PowerShell host, as opposed to
        PowerShell streams, its text color can be set via parameter -HostTextColor. Any valid
        PowerShell console color can be used;
 
    3) The message format: The format of the message can be set via parameter -MessageFormat.
 
.PARAMETER Message
The message to be logged.
 
.PARAMETER HostTextColor
The ForegroundColor the message will be written in, if the message is written to the host.
 
If the message is written to a PowerShell stream, such as the Information stream, this color is
ignored.
 
Acceptable values are: 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta',
'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White'.
 
.PARAMETER MessageFormat:
A string that sets the format of the text that will be logged. If MessageFormat is not
specified explicitly when calling Write-LogMessage the MessageFormat from the logger configuration
will be used.
 
Text enclosed in curly braces, {...}, represents the name of a field which will be included in
the logged text. The field names are not case sensitive.
         
Any other text, not enclosed in curly braces, will be treated as a string literal and will appear
in the logged text exactly as specified.
         
Leading spaces in the MessageFormat string will be retained when the text is written to the
log to allow log messages to be indented. Trailing spaces in the MessageFormat string will be
removed, and will not be written to the log.
         
Possible field names are:
    {Message} : The supplied text message to write to the log;
 
    {Timestamp} : The date and time the log message is recorded.
 
                    The Timestamp field may include an optional datetime format string inside
                    the curly braces, following the field name and separated from it by a
                    colon, ':'. For example, '{Timestamp:T}'.
                             
                    Any .NET datetime format string is valid. For example, "{Timestamp:d}" will
                    format the timestamp using the short date pattern, which is "MM/dd/yyyy" in
                    the US.
                             
                    While the field names in the MessageFormat string are NOT case sentive the
                    datetime format string IS case sensitive. This is because .NET datetime
                    format strings are case sensitive. For example, "d" is the short date
                    pattern while "D" is the long date pattern.
                             
                    The Timestamp field may be specified without any datetime format string. For
                    example, '{Timestamp}'. In that case the default datetime format string,
                    'yyyy-MM-dd HH:mm:ss.fff', will be used;
 
    {CallerName} : The name of the function or script that is writing to the log.
 
                    When determining the caller name all functions in the Pslogg module will be
                    ignored; the caller name will be the external function or script that calls
                    the Pslogg module to write to the log.
                             
                    If a function is writing to the log the function name will be displayed. If
                    the log is being written to from a script file, outside any function, the name
                    of the script file will be displayed. If the log is being written to manually
                    from the Powershell console then '[CONSOLE]' will be displayed;
 
    {CallerLineNumber} : The line number within the script that is writing to the log.
     
                    When determining the caller all functions in the Pslogg module will be
                    ignored; the caller will be the external function or script that calls the
                    Pslogg module to write to the log.
                             
                    If the log is being written to manually from the Powershell console then
                    '[NONE]' will be displayed;
 
    {Category} : The Message Category. If no Message Category is explicitly specified when
                    calling Write-LogMessage the default Category from the logger configuration
                    will be used;
 
    {MessageLevel} : The Message Level at which the message is being recorded. For example, the
                    message may be an Error message or a Debug message. The MessageLevel will
                    always be displayed in upper case.
 
.PARAMETER MessageLevel
A string that specifies the Message Level of the message. Possible values are the LogLevels:
    VERBOSE
    DEBUG
    INFORMATION
    WARNING
    ERROR
 
The Message Level is compared to the LogLevel in the logger configuration. If the Message Level
is the same as or lower than the LogLevel the message will be logged. If the Message Level is
higher than the LogLevel the message will not be logged.
 
For example, if the LogLevel is INFORMATION then only messages with a Message Level of
INFORMATION, WARNING or ERROR will be logged. Messages with a Message Level of DEBUG or
VERBOSE will not be logged, as those levels are higher than INFORMATION.
 
-MessageLevel cannot be specified at the same time as one of the Message Level switch parameters:
-IsVerbose, -IsDebug, -IsInformation, -IsWarning and -IsError. Either -MessageLevel can be
specified or one or the Message Level switches can be specified but not both.
 
In addition to determining whether the message will be logged or not, -MessageLevel has the
following effects:
 
    1) If the message is set to be written to a PowerShell stream it determines which stream the
        message will be written to: The Verbose stream, the Debug stream, the Information stream,
    the Warning stream or the Error stream;
 
    2) If the message is set to be written to the host and the -HostTextColor parameter is not
        specified -MessageLevel determines the ForegroundColor the message will be written in.
        The appropriate color is read from the logger configuration HostTextColor hash table.
        For example, if the -MessageLevel is ERROR the text ForegroundColor will be set to the
        color specified by logger configuration HostTextColor.Error;
 
    3) The {MessageLevel} placeholder in the MessageFormat string, if present, will be replaced
        by the -MessageLevel text. For example, if -MessageLevel is ERROR the {MessageLevel}
        placeholder will be replaced by the text 'ERROR'.
 
Using the Message Level switches, for example -IsDebug, to set the Message Level is more concise
than using -MessageLevel and specifying the Message Level via text. However, -MessageLevel is
useful for setting the Message Level programmatically, based on the outcome of some process.
 
.PARAMETER IsVerbose
Sets the Message Level to VERBOSE.
 
-IsVerbose is one of the Message Level switch parameters. Only one Message Level switch may be set
at the same time. The Message Level switch parameters are:
    -IsVerbose, -IsDebug, -IsInformation, -IsWarning, -IsError.
 
.PARAMETER IsDebug
Sets the Message Level to DEBUG.
 
-IsDebug is one of the Message Level switch parameters. Only one Message Level switch may be set
at the same time. The Message Level switch parameters are:
    -IsVerbose, -IsDebug, -IsInformation, -IsWarning, -IsError.
 
.PARAMETER IsInformation
Sets the Message Level to INFORMATION.
 
-IsInformation is one of the Message Level switch parameters. Only one Message Level switch may be
set at the same time. The Message Level switch parameters are:
    -IsVerbose, -IsDebug, -IsInformation, -IsWarning, -IsError.
 
.PARAMETER IsWarning
Sets the Message Level to WARNING.
 
-IsWarning is one of the Message Level switch parameters. Only one Message Level switch may be set
at the same time. The Message Level switch parameters are:
    -IsVerbose, -IsDebug, -IsInformation, -IsWarning, -IsError.
 
.PARAMETER IsError
Sets the Message Level to ERROR.
 
-IsError is one of the Message Level switch parameters. Only one Message Level switch may be set
at the same time. The Message Level switch parameters are:
    -IsVerbose, -IsDebug, -IsInformation, -IsWarning, -IsError.
 
.PARAMETER Category
A string that specifies the Message Category of the message. Any string can be specified.
 
The Message Category can have a color specified in the configuration CategoryInfo hash table. If
the -HostTextColor parameter is not specified and the message is being written to the host,
the ForegroundColor will be set to the CategoryInfo color from the configuration.
 
For example, if the -Category is 'Success' the text ForegroundColor will be set to the logger
configuration CategoryInfo.Success.Color.
 
-Category will default to the configuration CategoryInfo name which has IsDefault set.
 
.PARAMETER WriteToHost
A switch parameter that, if set, will write the message to the host, as opposed to one of the
PowerShell streams such as Error or Warning.
 
-WriteToHost overrides the logger configuration setting WriteToHost.
 
-WriteToHost and -WriteToStreams cannot both be set at the same time.
 
.PARAMETER WriteToStreams
A switch parameter that complements -WriteToHost. If set the message will be written to a
PowerShell stream.
 
-WriteToStreams overrides the logger configuration setting WriteToHost.
 
Which PowerShell stream is written to is determined by the Message Level, which may be set via
the -MessageLevel parameter or by one of the Message Level switch parameters:
-IsVerbose, -IsDebug, -IsInformation, -IsWarning or -IsError.
 
-WriteToHost and -WriteToStreams cannot both be set at the same time.
 
.PARAMETER WriteToFile
A switch parameter that, if set, will write the message to a log file. The log file written to
depends on the LogFile configuration settings.
 
-WriteToFile overrides the logger configuration settings LogFile.WriteFromScript or
LogFile.WriteFromHost, depending on whether Write-LogMessage is called from a script or module, or
from the PowerShell host.
 
-WriteToFile and -DoNotWriteToFile cannot both be set at the same time.
 
.PARAMETER DoNotWriteToFile
A switch parameter that, if set, will prevent the message being written to a log file.
 
-DoNotWriteToFile overrides the logger configuration settings LogFile.WriteFromScript or
LogFile.WriteFromHost, depending on whether Write-LogMessage is called from a script or module, or
from the PowerShell host.
 
-WriteToFile and -DoNotWriteToFile cannot both be set at the same time.
 
.EXAMPLE
Write error message:
 
    try
    {
        ...
    }
    catch [System.IO.FileNotFoundException]
    {
        Write-LogMessage -Message "Error while updating file: $_.Exception.Message" -IsError
    }
 
.EXAMPLE
Write debug message:
 
    Write-LogMessage "Updating user settings for $userName..." -IsDebug
 
The -Message parameter is optional.
 
.EXAMPLE
Write, specifying the MessageLevel rather than using a Message Level switch:
 
    Write-LogMessage "Updating user settings for $userName..." -MessageLevel 'DEBUG'
 
.EXAMPLE
Write message with a certain category:
 
    Write-LogMessage 'File copy completed successfully.' -Category 'Success' -IsInformation
 
.EXAMPLE
Write message to the PowerShell host in a specified color:
 
    Write-LogMessage 'Updating user settings...' -WriteToHost -HostTextColor Cyan
 
The MessageLevel wasn't specified so it will default to INFORMATION.
 
.EXAMPLE
Write message to the Debug PowerShell stream, rather than to the host:
 
    Write-LogMessage 'Updating user settings...' -WriteToStreams -IsDebug
 
.EXAMPLE
Write message to both the PowerShell host and a log file (with the log file name specified via
the LogFile configuration):
 
    Write-LogMessage 'Updating user settings...' -WriteToHost -WriteToFile
 
The MessageLevel wasn't specified so it will default to INFORMATION.
 
.EXAMPLE
Write message with a custom message format which only applies to this one message:
 
    Write-LogMessage "***** Running on server: $serverName *****" -MessageFormat '{message}'
 
The message written to the log will only include the specified message text. It will not
include other fields, such as {Timestamp} or {CallerName}.
 
.LINK
Get-LogConfiguration
 
.LINK
Set-LogConfiguration
 
.LINK
Reset-LogConfiguration
 
#>

function Write-LogMessage     
{
    [CmdletBinding(DefaultParameterSetName='MessageLevelText')]
    Param
    (
        [Parameter(Mandatory=$False, 
                    Position=0)]
        [string]$Message,

        [Parameter(Mandatory=$False)]
        [ValidateScript({Private_ValidateHostColor $_})]
        [string]$HostTextColor,      

        [Parameter(Mandatory=$False)]
        [string]$MessageFormat,      

        [Parameter(Mandatory=$False,
                    ParameterSetName='MessageLevelText')]
        [ValidateScript({ Private_ValidateLogLevel -LevelToTest $_ -ExcludeOffLevel })]
        [string]$MessageLevel,

        [Parameter(Mandatory=$False,
                    ParameterSetName='MessageLevelSwitches')]
        [switch]$IsVerbose, 

        [Parameter(Mandatory=$False,
                    ParameterSetName='MessageLevelSwitches')]
        [switch]$IsDebug, 

        [Parameter(Mandatory=$False,
                    ParameterSetName='MessageLevelSwitches')]
        [switch]$IsInformation, 

        [Parameter(Mandatory=$False,
                    ParameterSetName='MessageLevelSwitches')]
        [switch]$IsWarning,

        [Parameter(Mandatory=$False,
                    ParameterSetName='MessageLevelSwitches')]
        [switch]$IsError, 

        [Parameter(Mandatory=$False)]
        [string]$Category,

        [Parameter(Mandatory=$False)]
        [switch]$WriteToHost,      

        [Parameter(Mandatory=$False)]
        [switch]$WriteToStreams,      

        [Parameter(Mandatory=$False)]
        [switch]$WriteToFile,      

        [Parameter(Mandatory=$False)]
        [switch]$DoNotWriteToFile
    )

    Private_ValidateSwitchParameterGroup -SwitchList $IsVerbose,$IsDebug,$IsInformation,$IsWarning,$IsError `
        -ErrorMessage 'Only one Message Level switch parameter may be set when calling the function. Message Level switch parameters: -IsVerbose, -IsDebug, -IsInformation, -IsWarning, -IsError'

    Private_ValidateSwitchParameterGroup -SwitchList $WriteToHost,$WriteToStreams `
        -ErrorMessage 'Only one Destination switch parameter may be set when calling the function. Destination switch parameters: -WriteToHost, -WriteToStreams'
        
    Private_ValidateSwitchParameterGroup -SwitchList $WriteToFile,$DoNotWriteToFile `
    -ErrorMessage 'Only one Enable File switch parameter may be set when calling the function. Destination switch parameters: -WriteToFile, -DoNotWriteToFile'
    
    $Timestamp = Get-Date
    $CallerName = ''
    $TextColor = $Null
    $CallerLineNumber = ''

    $messageFormatInfo = $script:_messageFormatInfo
    if ($MessageFormat)
    {
        $messageFormatInfo = Private_GetMessageFormatInfo $MessageFormat
    }

    $CallerInfo = Private_GetCallerInfo
    if ($CallerInfo)
    {
        if ($CallerInfo.Name)
        {
            $CallerName = $CallerInfo.Name
        }
        if ($CallerInfo.LineNumber)
        {
            $CallerLineNumber = $CallerInfo.LineNumber
        }
    }

    # Parameter sets mean either $MessageLevel is supplied or a message level switch, such as
    # -IsError, but not both. Of course, they're all optional so none have to be specified, in
    # which case we set the default values:

    if ($IsError.IsPresent)
    {
        $MessageLevel = 'ERROR'
    }
    elseif ($IsWarning.IsPresent)
    {
        $MessageLevel = 'WARNING'
    }
    elseif ($IsInformation.IsPresent)
    {
        $MessageLevel = 'INFORMATION'
    }
    elseif ($IsDebug.IsPresent)
    {
        $MessageLevel = 'DEBUG'
    }
    elseif ($IsVerbose.IsPresent)
    {
        $MessageLevel = 'VERBOSE'
    }

    # Default.
    if (-not $MessageLevel)
    {
        $MessageLevel = 'INFORMATION'
    }    

    $configuredLogLevelValue = $script:_logLevels[$script:_logConfiguration.LogLevel]
    $messageLogLevelValue = $script:_logLevels[$MessageLevel]
    if ($messageLogLevelValue -gt $configuredLogLevelValue)
    {
        return
    }

    # Long-winded logic because we want either of the local parameters to override the
    # configuration setting: If either of the parameters is set ignore the configuration.
    $LogTarget = ''
    if ($WriteToHost.IsPresent)
    {
        $LogTarget = 'Host'
    }
    elseif ($WriteToStreams.IsPresent)
    {
        $LogTarget = 'Streams'
    }
    elseif ($script:_logConfiguration.WriteToHost)
    {
        $LogTarget = 'Host'
    }
    else
    {
        $LogTarget = 'Streams'
    }

    $configuredCategories = @{}
    if ($script:_logConfiguration.ContainsKey('CategoryInfo'))
    {
        $configuredCategories = $script:_logConfiguration.CategoryInfo
    }

    if ([string]::IsNullOrWhiteSpace($Category) -and $configuredCategories)
    {
        $Category = $configuredCategories.Keys.Where( 
            { $configuredCategories[$_] -is [hashtable] `
                -and $configuredCategories[$_].ContainsKey('IsDefault') `
                -and $configuredCategories[$_]['IsDefault'] -eq $True }, 
            'First', 1)
    }
    if ($Category)
    {
        $Category = $Category.Trim()
    }

    $textToLog = $ExecutionContext.InvokeCommand.ExpandString($messageFormatInfo.WorkingFormat)

    if ($LogTarget -eq 'Host')
    {
        if ($HostTextColor)
        {
            $TextColor = $HostTextColor
        }
        elseif ($Category -and $configuredCategories `
            -and $configuredCategories.ContainsKey($Category) `
            -and $configuredCategories[$Category].ContainsKey('Color'))
        {
            $TextColor = $configuredCategories[$Category].Color
        }
        else
        {
            switch ($MessageLevel)
            {
                ERROR    { $TextColor = $script:_logConfiguration.HostTextColor.Error; break }
                WARNING    { $TextColor = $script:_logConfiguration.HostTextColor.Warning; break }
                INFORMATION    { $TextColor = $script:_logConfiguration.HostTextColor.Information; break }
                DEBUG    { $TextColor = $script:_logConfiguration.HostTextColor.Debug; break }
                VERBOSE    { $TextColor = $script:_logConfiguration.HostTextColor.Verbose; break }
            }
        }

        if ($TextColor)
        {
            Write-Host $textToLog -ForegroundColor $TextColor
        }
        else
        {
            Write-Host $textToLog
        }
    }
    elseif ($LogTarget -eq 'Streams')
    {
        switch ($MessageLevel)
        {
            ERROR           { Write-Error $textToLog; break }
            WARNING         { Write-Warning $textToLog; break }
            INFORMATION     { Write-Information $textToLog; break }
            DEBUG           { Write-Debug $textToLog; break }
            VERBOSE         { Write-Verbose $textToLog; break }                            
        }
    }

    $writeToFileOverride = $Null
    if ($WriteToFile)
    {
        $writeToFileOverride = $True
    }
    elseif ($DoNotWriteToFile)
    {
        $writeToFileOverride = $False
    }

    if (-not (Private_ShouldWriteToFile -CallerName $CallerName -WriteToFileOverride $writeToFileOverride))
    {
        return
    }

    $overwriteLogFile = $False
    if ($script:_logConfiguration.LogFile.ContainsKey('Overwrite'))
    {
        $overwriteLogFile = $script:_logConfiguration.LogFile.Overwrite
    }
    if ($overwriteLogFile -and (-not $script:_logFileOverwritten))
    {
        Set-Content -Path $script:_logConfiguration.LogFile.FullPathReadOnly -Value $textToLog
        $script:_logFileOverwritten = $True
    }
    else
    {
        Add-Content -Path $script:_logConfiguration.LogFile.FullPathReadOnly -Value $textToLog
    }
}

<#
.SYNOPSIS
Gets the details of the function or script calling into this module.
 
.DESCRIPTION
Gets the details of the function or script calling into this module. It returns both the name
of the calling function or script and the calling line number within that script.
 
When determining the caller, this function will skip any functions in this script, returning the
details of the first function it encounters outside this script. This allows other functions in
this script to perform their own actions while being able to call down to this function to
determine which external function or script called them.
 
This function walks up the call stack until it finds a stack frame where the ScriptName is not the
filename of this module.
 
.OUTPUTS
Hashtable.
 
The function outputs a hashtable with two keys:
 
    Name: The name of the first calling function encountered outside this script. If this
            function is called from the root of a script, outside of a function, the name will be
            "Script <script file name>". The script file name will be just the short file name,
            without a path. If this function is called from the PowerShell console the name will
            be "[CONSOLE]". If no caller is found outside this script the name will be "----".
            If, for some reason, it's not possible to read the call stack the name will be
            "[UNKNOWN CALLER]".
    LineNumber: The caller line number, if this function is called from a function or script. If
            this function is called from the PowerShell console, or if no caller is found outside
            this script, or if it's not possible to read the call stack, the LineNumber will be
            "[NONE]".
 
.NOTES
This function is NOT intended to be exported from this module.
 
#>

function Private_GetCallerInfo()
{
    $callerInfo = @{}
    $callerInfo.Name = $script:_constCallerUnknown
    $callerInfo.LineNumber = $script:_constCallerLineNumberUnknown

    $callStack = Get-PSCallStack
    if ($null -eq $callStack -or $callStack.Count -eq 0)
    {
        return $callerInfo
    }
    
    # Stack frame 0 is this function. Increasing the index takes us further up the call stack,
    # further away from this function.
    $thisFunctionStackFrame = $callStack[0]
    $thisModuleFileName = $thisFunctionStackFrame.ScriptName
    $stackFrameFileName = $thisModuleFileName
    $stackFrameLineNumber = $script:_constCallerLineNumberUnknown
    # Skip this function in the call stack as we've already read it. There must be at least two
    # stack frames in the call stack as something must call this function, so it's safe to skip
    # the first stack frame.
    $i = 1
    $stackFrameFunctionName = $script:_constCallerFunctionUnknown
    while ($stackFrameFileName -eq $thisModuleFileName -and $i -lt $callStack.Count)
    {
        $stackFrame = $callStack[$i]
        $stackFrameFileName = $stackFrame.ScriptName
        $stackFrameFunctionName = $stackFrame.FunctionName
        $lineNumber = $stackFrame.ScriptLineNumber
        if (-not $lineNumber)
        {
            $stackFrameLineNumber = $script:_constCallerLineNumberUnknown
        }
        else 
        {
            $stackFrameLineNumber = "$lineNumber"
        }
        $i++
    }
    
    if ($null -eq $stackFrameFileName)
    {
        $callerInfo.Name = $script:_constCallerConsole
        $callerInfo.LineNumber = $script:_constCallerLineNumberUnknown
        return  $callerInfo
    }

    $callerInfo.Name = $stackFrameFunctionName
    $callerInfo.LineNumber = $stackFrameLineNumber

    if ($stackFrameFunctionName -eq '<ScriptBlock>')
    {
        $scriptFileNameWithoutPath = (Split-Path -Path $stackFrameFileName -Leaf)
        $callerInfo.Name = "Script $scriptFileNameWithoutPath"
    }
    
    return $callerInfo
}

<#
.SYNOPSIS
Indicates whether a log message should be written to a log file.
 
.DESCRIPTION
Indicates whether a log message should be written to a log file. This depends on whether the
message is being written from a script or from the PowerShell console, whether the
LogFile.WriteFromScript or LogFile.WriteFromHost configuration flags are set, and whether the
LogFile.Name configuration value is set.
 
.NOTES
This function is NOT intended to be exported from this module.
 
#>

function Private_ShouldWriteToFile([string]$CallerName, $WriteToFileOverride)
{
    # [string]::IsNullOrWhiteSpace returns True if key LogFile.Name doesn't exist.
    if ($Null -eq $script:_logConfiguration `
        -or -not $script:_logConfiguration.ContainsKey('LogFile') `
        -or [string]::IsNullOrWhiteSpace($script:_logConfiguration.LogFile.Name))
    {
        return $false
    }

    if (-not ($Null -eq $WriteToFileOverride))
    {
        return $WriteToFileOverride
    }

    # Assume that if the caller is unknown, this function is being called directly from the
    # PowerShell host rather than from a script.
    $calledFromHost = ($CallerName -eq $script:_constCallerConsole -or 
                        $CallerName -eq $script:_constCallerUnknown -or 
                        $CallerName -eq $script:_constCallerFunctionUnknown)

    # Called from script but LogFile.WriteFromScript is $false.
    if (-not $calledFromHost -and -not $script:_logConfiguration.LogFile.WriteFromScript)
    {
        return $false
    }

    # Called from host but LogFile.WriteFromHost is $false.
    if ($calledFromHost -and -not $script:_logConfiguration.LogFile.WriteFromHost)
    {
        return $false
    }

    return (Test-Path $script:_logConfiguration.LogFile.FullPathReadOnly -IsValid)
}

#endregion

# Set up initial conditions.
Reset-LogConfiguration

# Only export public functions. To simplify the exporting of public functions but not private
# ones public functions must follow the standard PowerShell naming convention,
# "<verb>-<singular noun>", while private functions must not contain a dash, "-".
Export-ModuleMember -Function *-*