Public/Find-LMLogAnomalyAlerts.ps1

<#
.SYNOPSIS
    Identifies and analyzes LogicMonitor alerts that have associated log anomalies.

.DESCRIPTION
    This cmdlet searches for alerts that have corresponding log anomalies within a specified time window.
    It analyzes log messages for sentiment, correlates them with alerts, and provides detailed information
    about the relationships between alerts and anomalous logs.

    The function performs sentiment analysis on log messages using a predefined list of negative terms
    and calculates both individual and aggregate sentiment scores for the logs associated with each alert.

.PARAMETER StartDate
    The beginning of the time range to search for alerts.
    Default: 14 days ago from current time
    
.PARAMETER EndDate
    The end of the time range to search for alerts.
    Default: Current time

.PARAMETER MaxPages
    Maximum number of pages to retrieve when querying log anomalies.
    Default: 10

.PARAMETER BatchSize
    Number of records to retrieve per page when querying log anomalies.
    Default: 500

.OUTPUTS
    Returns an array of custom objects containing:
    - Portal information
    - Alert details (ID, URL, type, severity, etc.)
    - Resource information
    - Instance details
    - Log analysis (count, sentiment scores)
    - Associated anomalous logs with timing and sentiment data

.EXAMPLE
    Find-LMLogAnomalyAlerts
    
    Searches for alert-anomaly correlations from the last 14 days to current time using default parameters.

.EXAMPLE
    Find-LMLogAnomalyAlerts -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date)
    
    Searches for alert-anomaly correlations from the last 7 days using custom date range.

.EXAMPLE
    Find-LMLogAnomalyAlerts -MaxPages 20 -BatchSize 1000
    
    Searches with increased page limit and batch size for larger environments.

.NOTES
    - Requires active connection to LogicMonitor portal (use Connect-LMAccount first)
    - Processes both cleared and active alerts
    - Matches logs and alerts within a 30-minute time window
    - Sentiment analysis is performed using predefined negative terms
    - Results are sorted by log count, total sentiment score, and severity
    
.LINK
    Module repo: https://github.com/stevevillardi/Logic.Monitor.SE

.LINK
    PSGallery: https://www.powershellgallery.com/packages/Logic.Monitor.SE
#>


Function Find-LMLogAnomalyAlerts {
    [CmdletBinding()]
    Param(
        [DateTime]$StartDate = (Get-Date).AddDays(-14),
        [DateTime]$EndDate = (Get-Date),
        [Int]$MaxPages = 10,
        [Int]$BatchSize = 500
    )
    Write-Information "Starting log anomaly alert matching process..."
    Write-Information "Date Range: $StartDate to $EndDate"

    # Define negative sentiment words commonly found in logs
    $negativeTerms = @(
        'error', 'fail', 'failed', 'failure', 'critical', 'severe', 'exception',
        'timeout', 'denied', 'rejected', 'invalid', 'fatal', 'crash', 'down',
        'outage', 'incident', 'issue', 'problem', 'warning', 'alarm', 'alert',
        'violation', 'exceeded', 'unavailable', 'unreachable', 'broken', 'corrupt'
    )

    # Function to calculate sentiment score
    function Get-LogSentiment {
        param (
            [string]$message
        )
        $score = 0
        $messageLower = $message.ToLower()
        
        foreach ($term in $negativeTerms) {
            if ($messageLower -match $term) {
                $score -= 1
            }
        }
        return $score
    }

    If($(Get-LMAccountStatus).Valid){
        $Portal = $(Get-LMAccountStatus).Portal
        Write-Information "Account validation successful. Retrieving alerts and anomalies..."
        
        $Alerts = Get-LMAlert -StartDate $StartDate -EndDate $EndDate -ClearedAlerts $true
        Write-Information "Retrieved $($Alerts.Count) alerts"
        
        $Anomalies = Get-LMLogMessage -Query '_anomaly.type="never_before_seen"' -StartDate $(Get-Date).AddDays(-3) -EndDate $(Get-Date) -Async -MaxPages $MaxPages -BatchSize $BatchSize
        Write-Information "Retrieved $($Anomalies.Count) anomalies"

        If($Anomalies -and $Alerts){
            Write-Information "Starting matching process..."
            $matchedEvents = @{}  # Changed to hashtable for grouping
            $processedCount = 0
            
            foreach ($log in $Anomalies) {
                $processedCount++
                if ($processedCount % 100 -eq 0) {
                    Write-Information "Processed $processedCount of $($Anomalies.Count) logs..."
                }

                $logTime = [DateTimeOffset]::FromUnixTimeMilliseconds($log.timestamp).DateTime
                $logResourceId = $log._resource.id
                
                Write-Debug "Processing log ID: $($log.id) for resource $logResourceId at time $logTime"
                
                $matchingAlerts = $Alerts | Where-Object {
                    $alertResourceId = $_.monitorObjectId
                    $alertStartTime = [DateTimeOffset]::FromUnixTimeSeconds($_.startEpoch).DateTime
                    
                    ($logResourceId -eq $alertResourceId) -and 
                    ([Math]::Abs(($logTime - $alertStartTime).TotalMinutes) -le 30)
                }
                
                if ($matchingAlerts) {
                    Write-Information "Found $($matchingAlerts.Count) matching alert(s) for log ID: $($log.id)"
                    foreach ($alert in $matchingAlerts) {
                        $alertKey = $alert.id
                        $timeDiff = [Math]::Abs(($logTime - [DateTimeOffset]::FromUnixTimeSeconds($alert.startEpoch).DateTime).TotalMinutes)
                        
                        $logEntry = @{
                            LogId = $log.id
                            LogTimestamp = $logTime
                            LogMessage = $log.message
                            TimeDifferenceMinutes = $timeDiff
                            SentimentScore = Get-LogSentiment -message $log.message
                        }

                        if (-not $matchedEvents.ContainsKey($alertKey)) {
                            $matchedEvents[$alertKey] = @{
                                AlertId = $alert.id
                                ResourceId = $logResourceId
                                AlertTimestamp = [DateTimeOffset]::FromUnixTimeSeconds($alert.startEpoch).DateTime
                                AlertType = $alert.type
                                AlertValue = $alert.alertValue
                                Severity = $alert.severity
                                DatasourceName = $alert.resourceTemplateName
                                ResourceName = $alert.monitorObjectName
                                InstanceName = $alert.instanceName
                                InstanceDescription = $alert.instanceDescription
                                DatapointName = $alert.datapointName
                                AssociatedLogs = @()
                                TotalSentimentScore = 0
                            }
                        }

                        $matchedEvents[$alertKey].AssociatedLogs += $logEntry
                        $matchedEvents[$alertKey].TotalSentimentScore += $logEntry.SentimentScore
                    }
                }
            }
            
            # Convert to array and sort by importance
            $sortedResults = $matchedEvents.Values | ForEach-Object {
                [PSCustomObject]@{
                    PortalName = $Portal
                    AlertId = $_.AlertId
                    AlertUrl = "https://$Portal.logicmonitor.com/santaba/uiv4/alerts/$($_.AlertId)"
                    ResourceId = $_.ResourceId
                    DatasourceName = $_.DatasourceName
                    ResourceName = $_.ResourceName
                    InstanceName = $_.InstanceName
                    InstanceDescription = $_.InstanceDescription
                    DatapointName = $_.DatapointName
                    AlertTimestamp = $_.AlertTimestamp
                    AlertType = $_.AlertType
                    Severity = $_.Severity
                    AlertValue = $_.AlertValue
                    LogCount = $_.AssociatedLogs.Count
                    TotalSentimentScore = $_.TotalSentimentScore
                    AverageSentimentScore = [math]::Round($_.TotalSentimentScore / $_.AssociatedLogs.Count, 2)
                    AssociatedLogs = $_.AssociatedLogs | Sort-Object SentimentScore
                }
            } | Sort-Object -Property @{Expression = "LogCount"; Descending = $true}, 
                                    @{Expression = "TotalSentimentScore"; Descending = $true},
                                    @{Expression = "Severity"; Descending = $true}

            Write-Information "Matching process complete. Found $($sortedResults.Count) unique alerts with associated logs."
            if ($sortedResults.Count -gt 0) {
                Write-Information "Returning matched events..."
                return $sortedResults
            } else {
                Write-Information "No matching events found within the specified time window."
                return $null
            }
        }
        Else {
            Write-Information "No anomalies or alerts found for the given date range."
            Return
        }
    }
    Else {
        Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
    }
}