src/public/Write-Log.ps1

<#
.SYNOPSIS
    Writes log messages with timestamps to console and optionally to a file.
 
.DESCRIPTION
    A logging function that can be used globally across all scripts. It supports
    different log levels (DEBUG, INFO, WARNING, ERROR) and allows logging to console
    and optionally to a log file. By default, messages are written to console only.
    To enable file logging, specify the LogPath parameter. The function supports
    configurable options for log file location and log rotation.
 
.PARAMETER Message
    The message to log. This parameter is mandatory but can be empty or blank.
    Empty messages will result in a log entry with just the timestamp and level.
 
.PARAMETER Level
    The log level for the message. Valid values are DEBUG, INFO, WARNING, and ERROR.
    Default is INFO.
 
.PARAMETER LogPath
    The path to the log file. If specified, messages will be written to both console
    and the log file. If not specified, messages are written to console only.
    If the directory does not exist, it will be created automatically.
 
.PARAMETER NoConsole
    Switch to disable console logging. Useful when you only want to log to a file.
 
.PARAMETER MaxFileSizeMB
    Maximum log file size in megabytes before rotation. Default is 10MB.
    When the file exceeds this size, it will be rotated. Only applies when LogPath is specified.
 
.PARAMETER MaxLogFiles
    Maximum number of rotated log files to keep. Default is 5.
    Older files beyond this count will be deleted. Only applies when LogPath is specified.
 
.PARAMETER MinLogLevel
    The minimum log level to output. Messages with a level below this will not be logged.
    Valid values are DEBUG, INFO, WARNING, and ERROR. Default is INFO.
 
.PARAMETER ForegroundColor
    Custom console color to use for the log message, overriding the default color scheme.
    Valid values are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow,
    Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White.
    This parameter has an alias 'Color' for convenience.
    Default colors by level: DEBUG=Cyan, INFO=White, WARNING=Yellow, ERROR=Red.
 
.EXAMPLE
    Write-Log -Message "Application started"
 
    Logs an INFO level message to the console only.
 
.EXAMPLE
    Write-Log -Message "An error occurred" -Level ERROR
 
    Logs an ERROR level message to the console only.
 
.EXAMPLE
    Write-Log -Message "Debug information" -Level DEBUG -MinLogLevel DEBUG
 
    Logs a DEBUG level message to the console when DEBUG level logging is enabled.
 
.EXAMPLE
    Write-Log -Message "Log to file" -LogPath "C:\Logs\myapp.log"
 
    Logs a message to both console and the specified log file. The directory will be created if it doesn't exist.
 
.EXAMPLE
    Write-Log -Message "File only message" -LogPath "C:\Logs\myapp.log" -NoConsole
 
    Logs a message only to the log file, not to the console.
 
.EXAMPLE
    Write-Log -Message "With rotation" -LogPath "C:\Logs\myapp.log" -MaxFileSizeMB 5 -MaxLogFiles 3
 
    Logs with custom rotation settings: rotate when file exceeds 5MB, keep only 3 archived logs.
 
.EXAMPLE
    Write-Log -Message "Success!" -ForegroundColor Green
 
    Logs an INFO level message in green color instead of the default white.
 
.EXAMPLE
    Write-Log -Message "Custom debug" -Level DEBUG -Color Magenta
 
    Logs a DEBUG level message in magenta using the Color alias, overriding the default cyan.
 
.EXAMPLE
    Write-Log -Message "Important error" -Level ERROR -ForegroundColor DarkRed
 
    Logs an ERROR level message in dark red instead of the default bright red.
 
.OUTPUTS
    None. This function writes to console and/or file but does not output to the pipeline.
 
.NOTES
    Log file format: YYYY-MM-DD HH:MM:SS [LEVEL] Message
    Rotated files are named with .1, .2, etc. suffixes (e.g., app.log.1, app.log.2)
 
#>

function Write-Log {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$Message = '',

        [Parameter(Mandatory = $false)]
        [ValidateSet('DEBUG', 'INFO', 'WARNING', 'ERROR')]
        [string]$Level = 'INFO',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$LogPath,

        [Parameter(Mandatory = $false)]
        [switch]$NoConsole,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 1000)]
        [int]$MaxFileSizeMB = 10,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 100)]
        [int]$MaxLogFiles = 5,

        [Parameter(Mandatory = $false)]
        [Alias('Color')]
        [ValidateSet('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 
                     'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 
                     'Red', 'Magenta', 'Yellow', 'White')]
        [string]$ForegroundColor
    )

    begin {

    }

    process {

        # Format timestamp as YYYY-MM-DD HH:MM:SS
        $Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'

        # Format the log message - trim trailing space if message is empty
        if ([string]::IsNullOrEmpty($Message)) {
            $FormattedMessage = "$Timestamp [$Level]"
        } else {
            $FormattedMessage = "$Timestamp [$Level] $Message"
        }

        # Console logging
        if (-not $NoConsole) {
            # Determine color to use: custom color if specified, otherwise default based on level
            if ($ForegroundColor) {
                $ConsoleColor = $ForegroundColor
            } else {
                $ConsoleColor = switch ($Level) {
                    'DEBUG'   { 'Cyan' }
                    'INFO'    { 'White' }
                    'WARNING' { 'Yellow' }
                    'ERROR'   { 'Red' }
                }
            }
            Write-Host $FormattedMessage -ForegroundColor $ConsoleColor
        }

        # File logging (only if LogPath is specified)
        if (-not [string]::IsNullOrEmpty($LogPath)) {
            # Ensure log directory exists
            $LogDirectory = Split-Path -Path $LogPath -Parent
            if (-not [string]::IsNullOrEmpty($LogDirectory) -and -not (Test-Path -Path $LogDirectory)) {
                New-Item -Path $LogDirectory -ItemType Directory -Force | Out-Null
            }

            # Check if log rotation is needed
            if (Test-Path -Path $LogPath) {
                $LogFile = Get-Item -Path $LogPath
                $FileSizeInMB = $LogFile.Length / 1MB

                if ($FileSizeInMB -ge $MaxFileSizeMB) {
                    # Perform log rotation
                    # Delete any log files at or beyond MaxLogFiles
                    $i = $MaxLogFiles
                    while ($true) {
                        $OldLogPath = "$LogPath.$i"
                        if (Test-Path -Path $OldLogPath) {
                            Remove-Item -Path $OldLogPath -Force
                            $i++
                        } else {
                            break
                        }
                    }

                    # Shift existing rotated logs
                    for ($i = $MaxLogFiles - 1; $i -ge 1; $i--) {
                        $CurrentPath = "$LogPath.$i"
                        $NewPath = "$LogPath.$($i + 1)"
                        if (Test-Path -Path $CurrentPath) {
                            Move-Item -Path $CurrentPath -Destination $NewPath -Force
                        }
                    }

                    # Rename current log to .1
                    Move-Item -Path $LogPath -Destination "$LogPath.1" -Force
                }
            }

            # Append message to log file
            Add-Content -Path $LogPath -Value $FormattedMessage -Encoding UTF8
        }
    }

    end {
    }
}