Private/Notify.ps1

#Requires -Version 5.1

function Get-SMBeatNotifyStatePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot
    )

    Join-Path $DataRoot 'notify.state.json'
}

function Add-SMBeatCollectorEvent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot,
        [Parameter(Mandatory = $true)]
        [string]$Kind,
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    try {
        $obj = [PSCustomObject]@{
            ts      = (ConvertTo-SMBeatUtcString -DateTime (Get-SMBeatUtcNow))
            kind    = $Kind
            message = $Message
            host    = $env:COMPUTERNAME
        }
        $line = '{0}{1}' -f (ConvertTo-Json -InputObject $obj -Depth 4 -Compress), [Environment]::NewLine
        $path = Get-SMBeatEventsPath -DataRoot $DataRoot
        Add-SMBeatTextFile -Path $path -Text $line
        if ((Test-Path -LiteralPath $path) -and ((Get-Item -LiteralPath $path).Length -gt 1048576)) {
            $kept = New-Object System.Collections.Generic.List[string]
            foreach ($old in [System.IO.File]::ReadLines($path, $script:SMBeatUtf8NoBom)) {
                if (-not [string]::IsNullOrWhiteSpace($old)) {
                    [void]$kept.Add($old)
                }
            }
            if ($kept.Count -gt 1500) {
                $start = $kept.Count - 1500
                $text = ($kept.GetRange($start, 1500) -join [Environment]::NewLine) + [Environment]::NewLine
                Write-SMBeatTextFile -Path $path -Text $text
            }
        }
    }
    catch {
    }
}

function Read-SMBeatCollectorEvents {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot,
        [datetime]$StartUtc,
        [datetime]$EndUtc
    )

    $path = Get-SMBeatEventsPath -DataRoot $DataRoot
    if (-not (Test-Path -LiteralPath $path)) {
        return @()
    }

    $list = New-Object System.Collections.Generic.List[object]
    foreach ($line in [System.IO.File]::ReadLines($path, $script:SMBeatUtf8NoBom)) {
        if ([string]::IsNullOrWhiteSpace($line)) {
            continue
        }
        try {
            $obj = ConvertFrom-SMBeatJson -Json $line
        }
        catch {
            continue
        }
        $ts = [datetime]::MinValue
        if ($obj.PSObject.Properties['ts'] -and $obj.ts) {
            $ts = ConvertFrom-SMBeatUtcString -Value ([string]$obj.ts)
        }
        if ($ts -eq [datetime]::MinValue) {
            continue
        }
        if ($ts -lt $StartUtc -or $ts -ge $EndUtc) {
            continue
        }
        $kind = ''
        $message = ''
        $hostName = ''
        if ($obj.PSObject.Properties['kind'] -and $obj.kind) {
            $kind = [string]$obj.kind
        }
        if ($obj.PSObject.Properties['message'] -and $obj.message) {
            $message = [string]$obj.message
        }
        if ($obj.PSObject.Properties['host'] -and $obj.host) {
            $hostName = [string]$obj.host
        }
        $list.Add([PSCustomObject]@{
            Utc     = $ts
            Kind    = $kind
            Message = $message
            Host    = $hostName
        }) | Out-Null
    }

    $list.ToArray()
}

function Write-SMBeatLog {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot,
        [Parameter(Mandatory = $true)]
        [string]$Message,
        [ValidateSet('INFO', 'WARN', 'ERROR')]
        [string]$Level = 'INFO'
    )

    $line = '{0} {1} {2}{3}' -f (ConvertTo-SMBeatUtcString -DateTime (Get-SMBeatUtcNow)), $Level, $Message, [Environment]::NewLine
    Add-SMBeatTextFile -Path (Get-SMBeatLogPath -DataRoot $DataRoot) -Text $line
}

function Test-SMBeatNotifyConfigured {
    param($Config)

    if ($null -eq $Config) {
        return $false
    }
    if (-not $Config.PSObject.Properties['MailTo'] -or [string]::IsNullOrWhiteSpace([string]$Config.MailTo)) {
        return $false
    }
    if (-not $Config.PSObject.Properties['SmtpServer'] -or [string]::IsNullOrWhiteSpace([string]$Config.SmtpServer)) {
        return $false
    }
    $true
}

function Get-SMBeatNoticeSubject {
    param(
        [string]$Kind,
        [string]$HostName
    )

    if ([string]::IsNullOrWhiteSpace($HostName)) {
        $HostName = $env:COMPUTERNAME
    }

    switch ($Kind) {
        'registered' { return ('SMBeat {0} collector registered' -f $HostName) }
        'started' { return ('SMBeat {0} collector started' -f $HostName) }
        'stopped' { return ('SMBeat {0} collector stopped' -f $HostName) }
        default { return ('SMBeat {0} collector error' -f $HostName) }
    }
}

function Test-SMBeatNotifyRateLimit {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot,
        [Parameter(Mandatory = $true)]
        [string]$Key,
        [int]$QuietSec = 900
    )

    if ($QuietSec -le 0) {
        return $false
    }

    $path = Get-SMBeatNotifyStatePath -DataRoot $DataRoot
    $json = Read-SMBeatTextFile -Path $path
    if ($null -eq $json) {
        return $false
    }

    $state = $null
    try {
        $state = ConvertFrom-SMBeatJson -Json $json
    }
    catch {
        return $false
    }

    if (-not $state -or -not $state.PSObject.Properties['Key'] -or [string]$state.Key -ne $Key) {
        return $false
    }
    if (-not $state.PSObject.Properties['Utc'] -or -not $state.Utc) {
        return $false
    }

    $last = ConvertFrom-SMBeatUtcString -Value ([string]$state.Utc)
    $age = (Get-SMBeatUtcNow) - $last
    $age.TotalSeconds -lt $QuietSec
}

function Save-SMBeatNotifyState {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot,
        [Parameter(Mandatory = $true)]
        [string]$Key
    )

    $obj = [PSCustomObject]@{
        Key = $Key
        Utc = (ConvertTo-SMBeatUtcString -DateTime (Get-SMBeatUtcNow))
    }
    Write-SMBeatTextFile -Path (Get-SMBeatNotifyStatePath -DataRoot $DataRoot) -Text (ConvertTo-SMBeatJson -InputObject $obj)
}

function Send-SMBeatMailMessage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$To,
        [Parameter(Mandatory = $true)]
        [string]$SmtpServer,
        [string]$From,
        [Parameter(Mandatory = $true)]
        [string]$Subject,
        [Parameter(Mandatory = $true)]
        [string]$Text
    )

    if ([string]::IsNullOrWhiteSpace($From)) {
        $From = $To
    }

    Send-MailMessage -To $To -From $From -Subject $Subject -Body $Text -SmtpServer $SmtpServer -ErrorAction Stop
}

function Send-SMBeatNotice {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DataRoot,
        [ValidateSet('registered', 'started', 'stopped', 'error')]
        [string]$Kind = 'error',
        [Parameter(Mandatory = $true)]
        [string]$Message,
        $Config
    )

    $level = 'INFO'
    if ($Kind -eq 'error') {
        $level = 'ERROR'
    }
    elseif ($Kind -eq 'stopped') {
        $level = 'WARN'
    }
    $key = '{0}|{1}' -f $Kind, $Message
    $quiet = 900
    if ($Kind -eq 'registered' -or $Kind -eq 'started' -or $Kind -eq 'stopped') {
        $quiet = 60
    }
    $limited = Test-SMBeatNotifyRateLimit -DataRoot $DataRoot -Key $key -QuietSec $quiet
    if ($Kind -eq 'error' -and $limited) {
        return
    }

    Write-SMBeatLog -DataRoot $DataRoot -Message $Message -Level $level
    Add-SMBeatCollectorEvent -DataRoot $DataRoot -Kind $Kind -Message $Message

    if ($null -eq $Config) {
        $Config = Read-SMBeatConfig -DataRoot $DataRoot
    }
    if (-not (Test-SMBeatNotifyConfigured -Config $Config)) {
        if ($Kind -eq 'error') {
            Save-SMBeatNotifyState -DataRoot $DataRoot -Key $key
        }
        return
    }
    if ($limited) {
        Write-SMBeatLog -DataRoot $DataRoot -Message ('Mail skipped (quiet): {0}' -f $Kind) -Level 'INFO'
        return
    }

    $subject = Get-SMBeatNoticeSubject -Kind $Kind -HostName $env:COMPUTERNAME
    $text = @(
        $Message
        ''
        ('Host: {0}' -f $env:COMPUTERNAME)
        ('Kind: {0}' -f $Kind)
        ('Utc: {0}' -f (ConvertTo-SMBeatUtcString -DateTime (Get-SMBeatUtcNow)))
        ('Log: {0}' -f (Get-SMBeatLogPath -DataRoot $DataRoot))
    ) -join [Environment]::NewLine

    $to = [string]$Config.MailTo
    $smtp = [string]$Config.SmtpServer
    $from = ''
    if ($Config.PSObject.Properties['MailFrom'] -and $Config.MailFrom) {
        $from = [string]$Config.MailFrom
    }

    try {
        Send-SMBeatMailMessage -To $to -SmtpServer $smtp -From $from -Subject $subject -Text $text
        Save-SMBeatNotifyState -DataRoot $DataRoot -Key $key
        Write-SMBeatLog -DataRoot $DataRoot -Message ('Mail sent: {0}' -f $Kind) -Level 'INFO'
    }
    catch {
        Write-SMBeatLog -DataRoot $DataRoot -Message ('Mail failed: {0}' -f $_.Exception.Message) -Level 'WARN'
    }
}