Private/Write-EFLog.ps1

function Write-EFLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [ValidateSet('Debug', 'Information', 'Warning', 'Error')]
        [string]$Level = 'Information',

        [AllowNull()]
        [object]$Data,

        [string]$CorrelationId
    )

    $levels = @{
        Debug       = 0
        Information = 1
        Warning     = 2
        Error       = 3
    }

    if ($levels[$Level] -lt $levels[$script:EFConfiguration.LogLevel]) {
        return
    }

    Write-Verbose -Message ("[{0}] {1}" -f $Level, $Message)

    if ([string]::IsNullOrWhiteSpace([string]$script:EFConfiguration.LogPath)) {
        return
    }

    try {
        $expandedLogPath = [Environment]::ExpandEnvironmentVariables([string]$script:EFConfiguration.LogPath)
        $logPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($expandedLogPath)
        $parent = Split-Path -Parent $logPath
        if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) {
            $null = New-Item -ItemType Directory -Path $parent -Force -ErrorAction Stop
        }
        $pathItem = if (Test-Path -LiteralPath $logPath) {
            Get-Item -LiteralPath $logPath -Force -ErrorAction Stop
        }
        else {
            Get-Item -LiteralPath $parent -Force -ErrorAction Stop
        }
        while ($null -ne $pathItem) {
            if (($pathItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
                throw [System.Security.SecurityException]::new(
                    "The configured log path passes through a link or reparse point: '$logPath'."
                )
            }
            if ($pathItem -is [IO.FileInfo]) {
                $pathItem = $pathItem.Directory
            }
            else {
                $pathItem = $pathItem.Parent
            }
        }

        $entry = [ordered]@{
            timestampUtc = [DateTime]::UtcNow.ToString('o')
            level        = $Level
            message      = $Message
            computerName = $env:COMPUTERNAME
            processId    = $PID
            correlationId = $CorrelationId
            data         = $Data
        }

        $line = ($entry | ConvertTo-Json -Depth 8 -Compress) + [Environment]::NewLine
        $lineBytes = [Text.UTF8Encoding]::new($false).GetBytes($line)
        $written = $false
        $lastWriteError = $null
        foreach ($attempt in 1..3) {
            $stream = $null
            try {
                $stream = [IO.FileStream]::new(
                    $logPath,
                    [IO.FileMode]::Append,
                    [IO.FileAccess]::Write,
                    [IO.FileShare]::Read
                )
                $stream.Write($lineBytes, 0, $lineBytes.Length)
                $stream.Flush($true)
                $written = $true
                break
            }
            catch {
                $lastWriteError = $_.Exception
                if ($attempt -lt 3) {
                    [Threading.Thread]::Sleep(50 * $attempt)
                }
            }
            finally {
                if ($null -ne $stream) { $stream.Dispose() }
            }
        }
        if (-not $written) {
            throw $lastWriteError
        }
    }
    catch {
        Write-Warning "EndpointForge could not write to its configured log: $($_.Exception.Message)"
    }
}