SATLogger.psm1

# Global configuration variables
$Global:LogConfig = $null
$Global:MonitorConfig = $null
$Global:BrokerConfig = $null
$Global:Alerts = $null
function Get-MonitorCredentials(){
    if(Get-Command "Get-AutomationPSCredential" -CommandType Cmdlet -ErrorAction SilentlyContinue){
        # Running in Azure
        return $KeyData = Get-AutomationPSCredential -Name 'Monitor'
    }
    else{

    }
    
}

function Set-LogConfiguration{
<#
        .SYNOPSIS
            Defines a parameter set for the SATLogger.
 
        .DESCRIPTION
            SATLogger enables standardized log messages in JSON, CSV, or Text (Pipe delimited)
            Messages can be optionally routed to the console and external monitor.
 
        .PARAMETER Format
            [string] The format of the log message string (JSON, CSV, or Text).
 
        .PARAMETER JobName
            [string] The name of the job being logged. This should be a unique descriptive name
            that can serve as a key for searches.
 
        .PARAMETER LogToFile
            [bool] Boolean to log to a file.
 
        .PARAMETER LogDirectory
            [string] The directory to write logs into. Will be ignored if LogFile is specified.
 
        .PARAMETER LogFile
            [string] The full path to the log file, including the file name and extension.
 
        .PARAMETER LogToMonitor
            [bool] Boolean to route log messages to External Monitor.
         
        .PARAMETER MonitorLogLevel
            [int] Log threshold to capture in External Monitor. All logs below the selected threshold will be suppressed.
            Accepted levels: 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=CRITICAL.
 
        .PARAMETER LogToConsole
            [bool] Boolean to display log messages in the console. This is required for
            azure automation jobs.
 
        .PARAMETER LogLevel
            [int] Log threshold to capture. All logs below the selected threshold will be suppressed.
            Accepted levels: 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=CRITICAL.
 
        .PARAMETER RetentionDays
            [int] Number of days to retain old log files on disk. This is only applicable for SATLogger-
            managed log files. Not supported with the -Append or -LogFile switches.
 
        .PARAMETER Append
            [bool] Boolean to append to an existing log file.
 
        .PARAMETER Priority
            [int] Optional priority level for the job, 1-4. When set alongside -LogToMonitor:$true,
            alerts are routed to the Azure Function broker service instead of LogicMonitor. Omit to
            preserve legacy LogicMonitor routing.
 
        .PARAMETER Remediation
            [string] Optional free-text remediation guidance to include with alerts sent to the broker.
 
        .PARAMETER DocLink
            [string] Optional URL to runbook/documentation to include with alerts sent to the broker.
 
        .PARAMETER Contacts
            [string[]] Optional list of contacts (e.g. email addresses) responsible for this script,
            included with alerts sent to the broker so downstream integrations (e.g. Jira Operations)
            can notify or assign the right people.
 
        .EXAMPLE
            Set-LogConfiguration -Format JSON -JobName AddressBookUpdate -LogFile .\AddressBookUpdate.json -LogToMonitor:$true -LogToConsole:$true -LogLevel 2
 
        .EXAMPLE
            Set-LogConfiguration -JobName InstallHotfix
 
        .OUTPUTS
            None
#>

    param (
        [Parameter(HelpMessage="Log message format. Options are 'CSV','JSON', or 'Text.'")]
        [ValidateSet('CSV','JSON','Text')]
        [string]$Format = 'Text',
        
        [Parameter(HelpMessage="Name of the Job your are logging.", Mandatory=$true)]
        [string]$JobName,
        
        [Parameter(HelpMessage="Set to TRUE if you would like to output to the console. This should be TRUE for all Azure Automation Jobs")]
        [bool]$LogToConsole = $true,
        
        [Parameter(HelpMessage="Set to TRUE to route logs to external Monitor.")]
        [bool]$LogToMonitor = $false,
        
        [Parameter(HelpMessage="Log threshold to capture. All logs below the selected threshold will be suppressed, 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=CRITICAL.")]
        [ValidateSet(0,1,2,3,4)]
        [int]$MonitorLogLevel = 2,

        [Parameter(HelpMessage="Indicates if you would like to output to a file. Default is true. Set to False for most Azure automation jobs.")]
        [bool]$LogToFile = $true,

        [Parameter(HelpMessage="The directory to write logs into. Will be ignored if LogFile is specified.")]
        [string]$LogDirectory=$null,
        
        [Parameter(HelpMessage="A full or relative path to the log file, including the file name and extension.")]
        [string]$LogFile = $null,
        
        [Parameter(HelpMessage="Log threshold to capture. All logs below the selected threshold will be suppressed, 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=CRITICAL.")]
        [ValidateSet(0,1,2,3,4)]
        [int]$LogLevel = 1,
        
        [Parameter(HelpMessage="Number of days to retain past log files. A value of 0 will retain all logs.")]
        [int]$RetentionDays = 0,
        
        [Parameter(HelpMessage="Append to an existing log file. Default is false.")]
        [bool]$Append = $false,

        [Parameter(HelpMessage="Optional priority level (1-4). When set with -LogToMonitor:`$true, routes alerts to the broker service instead of LogicMonitor.")]
        [ValidateSet(1,2,3,4)]
        [int]$Priority = 0,

        [Parameter(HelpMessage="Optional remediation guidance to include with alerts sent to the broker.")]
        [string]$Remediation = $null,

        [Parameter(HelpMessage="Optional URL to runbook/documentation to include with alerts sent to the broker.")]
        [string]$DocLink = $null,

        [Parameter(HelpMessage="Optional list of contacts (e.g. email addresses) responsible for this script, included with alerts sent to the broker.")]
        [string[]]$Contacts = @()
    )
    # If LogToFile is true, define the log file path.
    if($LogToFile){

        if($LogFile -and $LogDirectory){
            Write-Output "Specify either LogFile or LogDirectory. Deferring to LogFile parameter value."
        }

        if($LogFile -and ($RetentionDays -gt 0)){
            Write-Output "Ambiguous option selected with LogFile path specified: RetentionDays."
            Write-Output "If you would like to enable log pruning, do not specify the LogFile value."
            Write-Output "The logger will automatically generate and maintain log files."
            Write-Output "Setting retention to 0 to disable log pruning."
            $RetentionDays = 0
        }
        if($Append -and ($RetentionDays -gt 0)){
            Write-Output "Ambiguous option selected with log pruning enabled: Append."
            Write-Output "Log pruning is not supported with the Append option."
            Write-Output "Setting retention to 0 to disable log pruning."
            $RetentionDays = 0           
        }

        if(-not $LogFile){

            if(-not $LogDirectory){
                try{
                    $LogDirectory = "$(Split-Path $MyInvocation.PSCommandPath -ErrorAction Stop)\Logs"
                }
                catch{
                    # Probably running interactively.
                    $LogDirectory = ".\Logs"
                }
            }

            if($Format -eq "Text"){
                $Extension = ".txt"
            }
            elseif($Format -eq "CSV"){
                $Extension = ".csv"
            }
            elseif($Format -eq "JSON"){
                $Extension = ".json"
            }

            # If the append option is selected, use the job name only.
            # Otherwise, append the date to the log file name.
            if($Append){
                $LogFileName = "$($JobName)$($Extension)"
            }
            else{
                $LogDate = (Get-Date -Format yyy-MM-dd)
                $LogFileName = "$($JobName)_$($LogDate)$($Extension)"
            }
            
            $LogFile = "$LogDirectory\$LogFileName"

        }


        if(-not (Test-Path $LogFile)){
            try{
                # Create the file if it doesn't exist.
                New-Item -ItemType File -Path $LogFile -Force -ErrorAction Stop | Out-Null

                # Write the CSV Header for new log files.
                if($Format -eq "CSV"){
                    [PSCustomObject]@{
                        "DateTime" = $Date
                        "JobName" = $LogConfig.JobName
                        "Severity" = $Type
                        "Message" = $Message
                        "Host" = $LogConfig.Host
                        "Script" = $LogConfig.Script
                    } | Select-Object DateTime,JobName,Severity,Message | Export-Csv $LogFile -NoTypeInformation
                }

            }
            catch{
                Write-Output "Unable to create log file $($LogFile). Exception: $($_.ErrorDetails.Message)"
                Write-Output "Streaming log to console only."
                $LogToFile = $false
            }
        }

        # Clean old log files
        if($RetentionDays -gt 0){

            if($LogDirectory -and $Extension){
                $LogsToPurge = Get-ChildItem $LogDirectory "$($JobName)*$($extension)" | Where-Object {$_.CreationTime -lt (Get-Date).AddDays(-$RetentionDays)}
                foreach($Item in $LogsToPurge){
                    try{
                        Remove-Item $Item.FullName -Force -ErrorAction Stop
                        Write-Output "Purged log based on retention policy: $($Item.FullName)"
                    }
                    catch{
                        Write-Output "Failed to purge log $($Item.FullName). Error: $($_.ErrorDetails.Message)"
                    }   
                }
            }

        }
    }

    # Configure the
    if($LogToMonitor){
        [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;

        if($Priority){
            # A Priority was supplied: route alerts to the Azure Function broker service
            # instead of LogicMonitor. The "api_monitor" secret stores the Function URL
            # as the username and the route token as the password.
            if(Get-Command "Get-AutomationPSCredential" -CommandType Cmdlet -ErrorAction SilentlyContinue){
                try{
                    $BrokerCredential = Get-AutomationPSCredential -Name "api_monitor" -ErrorAction Stop
                    $FunctionUrl = $BrokerCredential.Username
                    $RouteToken = $BrokerCredential.GetNetworkCredential().Password
                    Set-BrokerConfiguration -FunctionUrl $FunctionUrl -Token $RouteToken
                }
                catch{
                    Write-Output "Detected running in Azure. Failed to obtain the api_monitor credential object. Ensure it exists."
                }
            }
            else{
                if($BrokerCredential = Get-Secret -Name "api_monitor" -ErrorAction SilentlyContinue){
                    $FunctionUrl = $BrokerCredential.Username
                    $RouteToken = $BrokerCredential.GetNetworkCredential().Password
                    Set-BrokerConfiguration -FunctionUrl $FunctionUrl -Token $RouteToken
                }
                else{
                    Write-Output "No api_monitor secret found for $($ENV:USERNAME). Proceeding without logging to external broker."
                }
            }

            if(-not $Global:BrokerConfig){
                $LogToMonitor = $false
            }
        }
        else{

            # Check if we're running in Azure
            if(Get-Command "Get-AutomationPSCredential" -CommandType Cmdlet -ErrorAction SilentlyContinue){
                try{
                    # Pull connection vars from Azure
                    $Endpoint = Get-AutomationPSCredential -Name "MonitorEndpoint" -ErrorAction Stop
                    $KeyData = Get-AutomationPSCredential -Name 'Monitor' -ErrorAction Stop
                    $Url = $Endpoint.Username
                    $ApiKey = $KeyData.Username
                    $ApiSecret = $KeyData.GetNetworkCredential().Password
                    Set-MonitorConfiguration -Url $url -ApiKey $ApiKey -ApiSecret $ApiSecret
                }
                catch{
                    Write-Output "Detected running in Azure. Failed to obtain the Monitor or MonitorEndpoint credential object. Ensure they exist."
                }
            }
            else{

                # Check the secret vault for the "Monitor" secret
                if($MonitorCredentials = Get-Secret -Name "Monitor" -ErrorAction SilentlyContinue){
                    if($PSVersionTable.PSversion.Major -gt 5){
                        $ApiKey = $MonitorCredentials.apikey | ConvertFrom-SecureString -AsPlainText
                        $ApiSecret = $MonitorCredentials.apisecret | ConvertFrom-SecureString -AsPlainText
                        $Url = $MonitorCredentials.url | ConvertFrom-SecureString -AsPlainText
                    }
                    elseif($PSVersionTable.PSversion.Major -le 5){
                        $BString = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($MonitorCredentials.apikey)
                        $ApiKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BString)

                        $BString2 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($MonitorCredentials.apisecret)
                        $ApiSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BString2)

                        $BString3 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($MonitorCredentials.url)
                        $Url = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BString3)
                    }
                }
                elseif(Test-Path ~\.monitor){
                    # Check .monitor config file
                    try{
                        $MonitorJSON = Get-Content ~\.monitor | ConvertFrom-Json
                        $ApiKey = $MonitorJSON.apikey
                        $ApiSecret = $MonitorJSON.apisecret
                        $Url = $MonitorJSON.url
                    }
                    catch{
                        Write-Output "~\.monitor configuration file is inaccessible, in an invalid format, or missing configuration variables."
                    }
                }
                else{
                    Write-Output "No external monitoring configuration found for $($ENV:USERNAME). Proceeding without logging to external monitor."
                }
            }

            if($ApiKey -and $ApiSecret -and $url){
                $Monitor = @{
                    Url = $url
                    ApiKey = $ApiKey
                    ApiSecret = $ApiSecret
                }
                Set-MonitorConfiguration @Monitor
                $Global:Alerts = Get-MonitorAlerts
            }
            else{
                $LogToMonitor = $false
            }
        }
    }

    $Global:LogConfig =  @{
        "Format" = $Format
        "JobName" = $JobName
        "LogToFile" = $LogToFile
        "LogDirectory" = $LogDirectory
        "LogFile" = $LogFile
        "LogToMonitor" = $LogToMonitor
        "MonitorLogLevel" = $MonitorLogLevel
        "LogToConsole" = $LogToConsole
        "LogLevel" = $LogLevel
        "Host" = $env:COMPUTERNAME
        "Script" = $MyInvocation.PSCommandPath
        "Priority" = $Priority
        "Remediation" = $Remediation
        "DocLink" = $DocLink
        "Contacts" = $Contacts
    }
}

function New-LogMessage{  
<#
        .SYNOPSIS
            Writes log messages to one or more output channels.
 
        .DESCRIPTION
            New-LogMessage accepts a message string and a severity indicator
            to route to one or more output channels in the log configuration.
 
        .PARAMETER Severity
            [int] The severity of the message being logged. Accepted levels:
            0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=CRITICAL.
 
        .PARAMETER Message
            [string] The message to output.
 
        .EXAMPLE
            New-LogMessage -Severity 4 -Message "AUGGHHHHH!"
 
 
        .OUTPUTS
            None
#>

    param (
        [Parameter(HelpMessage="Severity of the message (0-4), 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=CRITICAL. Default is 1 (INFO).")]
        [ValidateSet(0,1,2,3,4)]
        [int]$Severity = 1,
        [Parameter(HelpMessage="The string you would like to append to the log.", Mandatory=$true)]
        [string]$Message
    )

    if(!($Global:LogConfig)){
        Write-Host "Log configuration undefined. Define your configuration with 'Set-LogConfiguration' to use this function."
        Write-Host "Setting a default log configuration."
        $JobGuid = New-Guid
        $LogConfig = Set-LogConfiguration -Format Text -JobName $JobGuid -LogFile ".\$($JobGuid).txt" -LogToMonitor:$false -LogToConsole:$true -LogLevel 1
    }

    $Date = get-date -Format "yyyy-MM-dd hh:mm:ss"
    $Type = $null
    switch ($Severity)
    {
        0 { $Type = "DEBUG"}
        1 { $Type = "INFO" }
        2 { $Type = "WARN" }
        3 { $Type = "ERROR" }
        4 { $Type = "CRITICAL"}
        default {$Type = "INFO" }
    }

    # Only log if the severity is greater than or equal to the severity defined in the config
    if($Severity -ge $LogConfig.LogLevel){
        
        # Set the log string based on the defined output type
        if($LogConfig.Format -eq "JSON"){
            $LogString = @{
                "DateTime" = $Date
                "JobName" = $LogConfig.JobName
                "Severity" = $Type
                "Message" = $Message
                "Host" = $LogConfig.Host
                "Script" = $LogConfig.Script
            } | ConvertTo-Json -Compress -Depth 3
        }
        elseif($LogConfig.Format -eq "CSV"){
            $LogString = [PSCustomObject]@{
                "DateTime" = $Date
                "JobName" = $LogConfig.JobName
                "Severity" = $Type
                "Message" = $Message
                "Host" = $LogConfig.Host
                "Script" = $LogConfig.Script
            } | Select-Object DateTime,JobName,Severity,Message | ConvertTo-Csv -NoHeader
        }
        elseif($LogConfig.Format -eq "Text"){
            $LogString = $Date + " | " + $LogConfig.JobName + " | " + $Type + " | " + $Message + " | " + $LogConfig.Host + " | " + $LogConfig.Script
        }

        # Console and Stream output:
        if($LogConfig.LogToConsole){
            Write-Host $LogString
        }

        # File output
        # Concurrent runs of the same job can share a log file, so writes are serialized
        # across processes with a named Mutex to avoid "file in use" errors.
        if($LogConfig.LogToFile){
            $MutexName = "Global\SATLogger_" + ($LogConfig.LogFile -replace '[\\/:]','_')
            $Mutex = New-Object System.Threading.Mutex($false, $MutexName)
            $Acquired = $false
            try{
                $Acquired = $Mutex.WaitOne(10000)
                if($Acquired){
                    $LogString | Out-File $LogConfig.LogFile -Append
                }
                else{
                    Write-Host "Timed out waiting for lock on log file $($LogConfig.LogFile). Dropping this file entry."
                }
            }
            catch{
                Write-Host "Failed to write to log file $($LogConfig.LogFile). Exception: $($_.Exception.Message)"
            }
            finally{
                if($Acquired){ $Mutex.ReleaseMutex() }
                $Mutex.Dispose()
            }
        }

        # Route message to external Monitor
        if($LogConfig.LogToMonitor){
            if($LogConfig.Priority){
                # Priority is set: route to the Azure Function broker service instead of LogicMonitor.
                # Handle when a separate threshold defined for external Monitor
                if($LogConfig.MonitorLogLevel){
                    if($Severity -ge $LogConfig.MonitorLogLevel){
                        Write-BrokerLog -Message $Message -Severity $Type
                    }
                }
                else{
                    Write-BrokerLog -Message $Message -Severity $Type
                }
            }
            else{
                # Handle when a separate threshold defined for external Monitor
                if($LogConfig.MonitorLogLevel){

                    if($Severity -ge $LogConfig.MonitorLogLevel){
                        if($Global:Alerts){
                            if($MatchAlert = $Global:Alerts | Where-Object {$_.script -eq $LogConfig.JobName -and $_.message -eq $Message -and $_.date -gt (Get-Date).AddHours(-23)}){
                                # Matching alert exists
                                New-LogMessage -Severity 1 -Message "Suppressing alert message to prevent duplicate alerts within 24 hours. Active alerts @ $($MatchAlert.date -join ',')"
                            }
                            else{
                                # No Matching alerts. Write alert to monitor
                                Write-MonitorLog -LogString $LogString
                            }
                        }
                        else{
                            # No outstanding alerts. Write alert to monitor
                            Write-MonitorLog -LogString $LogString
                        }
                    }
                }
                else{
                    Write-MonitorLog -LogString $LogString
                }
            }
        }
    }
}

function Set-MonitorConfiguration{
<#
        .SYNOPSIS
            Defines a parameter set for connecting to external monitor's log ingest API
 
        .DESCRIPTION
            Set-MonitorConfiguration is a function that defines a global hash table
            which contains the connection parameters for external monitor's ingest API. All
            parameters are required.
 
        .PARAMETER Url
            [string] The base URL for the monitor's rest API.
 
        .PARAMETER ApiKey
            [string] Monitor API Key.
 
        .PARAMETER ApiSecret
            [string] Monitor API Secret value.
 
        .EXAMPLE
            Set-MonitorConfiguration -ApiKey <API_KEY> -ApiSecret <API_Secret> -Url "https://mycompany.monitor.com/rest"
 
 
        .OUTPUTS
            None
#>

    param (
        [Parameter(HelpMessage="The base URL for the monitor API.", Mandatory=$true)]
        [string]$Url,
        [Parameter(HelpMessage="Monitor API Key.", Mandatory=$true)]
        [string]$ApiKey,
        [Parameter(HelpMessage="Monitor API Secret value.", Mandatory=$true)]
        [string]$ApiSecret 
    )

    $Global:MonitorConfig =  @{
        "BaseURL" = $Url
        "ApiKey" = $ApiKey
        "ApiSecret" = $ApiSecret
    }
}
    
function Write-MonitorLog{

    param (
        [Parameter(HelpMessage="The formatted log string to send to external monitor.", Mandatory=$true)]
        [string]$LogString
    )

    if(!($Global:MonitorConfig)){
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 2 -Message "External monitor is not configured. Define your configuration with 'Set-MonitorConfiguration' to log to external monitor."
        New-LogMessage -Severity 1 -Message "Disabled monitor logging in the log configuration."
    }

    $method = "POST"
    $base_url = $MonitorConfig.BaseURL
    $path = "/log/ingest"
    try{
        $request_data = ConvertTo-Json @(@{"msg"=$LogString;"_lm.resourceId"=@{"system.jobName"=$LogConfig.JobName};"jobName"=$LogConfig.JobName}) -Depth 4 -Compress
    }
    catch{
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 2 -Message "Unable to convert log message data to JSON. Disabling external monitor logging. Error: $($_.ErrorDetails.Message)"
        return
    }

    $api_key = $MonitorConfig.ApiKey
    $api_secret = $MonitorConfig.ApiSecret

    $credential = New-Object pscredential($api_key, (ConvertTo-SecureString $api_secret -AsPlainText -Force))

    # Get current time in milliseconds
    $epoch = [Math]::Round((New-TimeSpan -start (Get-Date -Date "1/1/1970") -end (Get-Date).ToUniversalTime()).TotalMilliseconds)

    # Concatenate Request Details
    $request_vars = $method + $epoch + $request_data + $path

    # Extract credentials
    $access_id = $credential.GetNetworkCredential().username
    $access_key = $credential.GetNetworkCredential().password

    # Construct Signature
    $hmac = New-Object System.Security.Cryptography.HMACSHA256
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($access_key)
    $signature_bytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($request_vars))
    $signature_hex = [System.BitConverter]::ToString($signature_bytes) -replace '-'
    $signature = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($signature_hex.ToLower()))
    $auth_key = "LMv1 $access_id`:$signature`:$epoch"

    $headers = @{
        'Content-Type'='application/json'
        'Authorization'=$auth_key
    }
    try{
        $response = Invoke-RestMethod -Uri "$($base_url)$($path)" -Method $method -Headers $headers -Body $request_data
        if($response.errmsg){
            $Global:LogConfig.LogToMonitor = $false
            New-LogMessage -Severity 2 -Message "External monitor returned an error: $($response.errmsg)."
            New-LogMessage -Severity 1 -Message "Disabled external monitor logging in the log configuration."
        } 
    }  
    catch{
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 3 -Message "Unable to route log message to external monitor. Error: $($_.ErrorDetails.Message)"
    }
}

function Set-BrokerConfiguration{
<#
        .SYNOPSIS
            Defines a parameter set for connecting to the Azure Function broker service.
 
        .DESCRIPTION
            Set-BrokerConfiguration is a function that defines a global hash table
            which contains the connection parameters for the broker service. The broker
            decouples SATLogger from LogicMonitor, forwarding alerts to whichever downstream
            integration (LogicMonitor, Jira Operations, etc) is configured for the route token.
 
        .PARAMETER FunctionUrl
            [string] The Azure Function URL, including any host/function key query string.
 
        .PARAMETER Token
            [string] The route token identifying which downstream integration should receive the alert.
 
        .EXAMPLE
            Set-BrokerConfiguration -FunctionUrl "https://mybroker.azurewebsites.net/api/log?code=..." -Token "abc123"
 
        .OUTPUTS
            None
#>

    param (
        [Parameter(HelpMessage="The Azure Function URL for the broker service.", Mandatory=$true)]
        [string]$FunctionUrl,
        [Parameter(HelpMessage="The route token identifying the downstream integration.", Mandatory=$true)]
        [string]$Token
    )

    $Global:BrokerConfig = @{
        "FunctionUrl" = $FunctionUrl
        "Token" = $Token
    }
}

function Write-BrokerLog{
<#
        .SYNOPSIS
            Sends a log message to the Azure Function broker service.
 
        .DESCRIPTION
            Posts a JSON payload to the broker endpoint defined in $Global:BrokerConfig.
            The broker looks for a "token" attribute to identify the route, and is
            responsible for forwarding the alert to the appropriate downstream system.
 
        .PARAMETER Message
            [string] The log message text.
 
        .PARAMETER Severity
            [string] The severity label (DEBUG, INFO, WARN, ERROR, CRITICAL).
 
        .OUTPUTS
            None
#>

    param (
        [Parameter(HelpMessage="The log message to send to the broker.", Mandatory=$true)]
        [string]$Message,
        [Parameter(HelpMessage="The severity label of the message.", Mandatory=$true)]
        [string]$Severity
    )

    if(!($Global:BrokerConfig)){
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 2 -Message "Broker service is not configured. Define your configuration with 'Set-BrokerConfiguration' to log to the broker."
        New-LogMessage -Severity 1 -Message "Disabled monitor logging in the log configuration."
        return
    }

    $Payload = @{
        "token" = $Global:BrokerConfig.Token
        "message" = $Message
        "severity" = $Severity
        "jobName" = $Global:LogConfig.JobName
        "host" = $Global:LogConfig.Host
        "script" = $Global:LogConfig.Script
        "priority" = $Global:LogConfig.Priority
        "remediation" = $Global:LogConfig.Remediation
        "docLink" = $Global:LogConfig.DocLink
        "contacts" = $Global:LogConfig.Contacts
        "dateTime" = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
    }

    try{
        $Body = $Payload | ConvertTo-Json -Compress -Depth 4
    }
    catch{
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 2 -Message "Unable to convert log message data to JSON. Disabling broker logging. Error: $($_.ErrorDetails.Message)"
        return
    }

    try{
        Invoke-RestMethod -Uri $Global:BrokerConfig.FunctionUrl -Method Post -Body $Body -ContentType "application/json" | Out-Null
    }
    catch{
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 3 -Message "Unable to route log message to broker endpoint. Error: $($_.Exception.Message)"
    }
}

function Get-MonitorAlerts{

    if(!($Global:MonitorConfig)){
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 2 -Message "External monitor is not configured. Define your configuration with 'Set-MonitorConfiguration' to log to external monitor."
        New-LogMessage -Severity 1 -Message "Disabled monitor logging in the log configuration."
        return
    }

    $method = "GET"
    $base_url = ((Split-Path $MonitorConfig.BaseURL).Replace("\","/")) + "/santaba/rest"
    $path = '/alert/alerts'
    $query = '?size=1000&filter=monitorObjectName~SATLogger Jobs'

    $api_key = $MonitorConfig.ApiKey
    $api_secret = $MonitorConfig.ApiSecret

    $credential = New-Object pscredential($api_key, (ConvertTo-SecureString $api_secret -AsPlainText -Force))

    # Get current time in milliseconds
    $epoch = [Math]::Round((New-TimeSpan -start (Get-Date -Date "1/1/1970") -end (Get-Date).ToUniversalTime()).TotalMilliseconds)

    # Concatenate Request Details
    $request_vars = $method + $epoch + $path

    # Extract credentials
    $access_id = $credential.GetNetworkCredential().username
    $access_key = $credential.GetNetworkCredential().password

    # Construct Signature
    $hmac = New-Object System.Security.Cryptography.HMACSHA256
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($access_key)
    $signature_bytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($request_vars))
    $signature_hex = [System.BitConverter]::ToString($signature_bytes) -replace '-'
    $signature = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($signature_hex.ToLower()))
    $auth_key = "LMv1 $access_id`:$signature`:$epoch"

    $headers = @{
        'Content-Type'='application/json'
        'Authorization'=$auth_key
    }
    try{
        $response = Invoke-RestMethod -Uri "$($base_url)$($path)$($query)" -Method $method -Headers $headers
        if($response.errmsg -ne "OK"){
            $Global:LogConfig.LogToMonitor = $false
            New-LogMessage -Severity 2 -Message "External monitor returned an error: $($response.errmsg)."
            New-LogMessage -Severity 1 -Message "Disabled external monitor logging in the log configuration."
        } 
        else{
            if($response.data.total -gt 0){
                $alerts = @()
                try{
                    foreach($item in $response.data.items){
                        $split = $item.alertValue.split('|').trim()
                        $alerts += @{
                            date = (Get-Date $split[0])
                            script = $split[1]
                            severity = $split[2]
                            message = $split[3]
                            host = $split[4]
                        }
                    }
                }
                catch{
                    New-LogMessage -Severity 2 -Message "Unable to parse alert data"
                }
            }
            return $alerts
        }
    }  
    catch{
        $Global:LogConfig.LogToMonitor = $false
        New-LogMessage -Severity 3 -Message "Unable to route log message to external monitor. Error: $($_.ErrorDetails.Message)"
    }
}