EventMonitor/TelemetryClient.ps1
|
# ── Telemetry Dispatcher ────────────────────────────────────────────────────── # Pluggable telemetry architecture. Event processors call TrackEvent/TrackTrace/ # TrackException without knowing where events go. The dispatcher routes to all # registered sinks (Application Insights, future: OTel, email, webhook, etc.). # # Built-in sink: Application Insights (loaded when connection string is present). # To add a sink: Register-TelemetrySink -Name 'MySink' -OnEvent { param($Name, $Props) ... } # To remove: Unregister-TelemetrySink -Name 'MySink' # ── Sink Registry ──────────────────────────────────────────────────────────── $script:TelemetrySinks = [ordered]@{} <# .SYNOPSIS Registers a telemetry sink that receives all tracked events/traces/exceptions. .DESCRIPTION A sink is a scriptblock that receives dispatched telemetry. Register multiple sinks to fan out events to different destinations simultaneously. Each sink receives these parameters via $args or named params in the scriptblock: - Type: 'Event', 'Trace', or 'Exception' - Name: Event name or trace message - Properties: Dictionary[string,string] of key-value pairs - Metrics: Dictionary[string,double] of numeric metrics (events only) - ErrorRecord: The original ErrorRecord (exceptions only) Sink exceptions are caught and logged — a failing sink never blocks others. .PARAMETER Name Unique name for this sink (used for management and logging). .PARAMETER OnDispatch Scriptblock to execute when telemetry is dispatched. .EXAMPLE # Log all events to a file Register-TelemetrySink -Name 'FileLog' -OnDispatch { param($Type, $Name, $Properties) "$Type : $Name" | Out-File -Append 'C:\Logs\events.txt' } .EXAMPLE # Send critical alerts via webhook Register-TelemetrySink -Name 'Webhook' -OnDispatch { param($Type, $Name, $Properties) if ($Properties['Severity'] -eq 'Critical') { Invoke-RestMethod -Uri 'https://hooks.example.com/alert' -Method Post -Body ($Properties | ConvertTo-Json) } } #> function Register-TelemetrySink { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [scriptblock]$OnDispatch ) $script:TelemetrySinks[$Name] = @{ Name = $Name OnDispatch = $OnDispatch Enabled = $true } Write-EMLog -Message "Telemetry sink '$Name' registered." } <# .SYNOPSIS Removes a registered telemetry sink. #> function Unregister-TelemetrySink { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name ) if ($script:TelemetrySinks.Contains($Name)) { $script:TelemetrySinks.Remove($Name) Write-EMLog -Message "Telemetry sink '$Name' unregistered." } } <# .SYNOPSIS Returns the list of registered telemetry sinks. #> function Get-TelemetrySinks { [CmdletBinding()] param() foreach ($key in $script:TelemetrySinks.Keys) { $sink = $script:TelemetrySinks[$key] [PSCustomObject]@{ Name = $sink.Name Enabled = $sink.Enabled } } } # ── Internal Dispatch ───────────────────────────────────────────────────────── function Invoke-TelemetryDispatch { param( [string]$Type, [string]$Name, [System.Collections.Generic.Dictionary[string, string]]$Properties, [System.Collections.Generic.Dictionary[string, double]]$Metrics, [System.Management.Automation.ErrorRecord]$ErrorRecord ) foreach ($key in @($script:TelemetrySinks.Keys)) { $sink = $script:TelemetrySinks[$key] if (-not $sink.Enabled) { continue } try { & $sink.OnDispatch $Type $Name $Properties $Metrics $ErrorRecord } catch { # A failing sink never blocks other sinks or the caller Write-EMLog -Message "Telemetry sink '$($sink.Name)' failed: $($_.Exception.Message)" -Level Error } } } # ── Application Insights Sink (Built-in) ───────────────────────────────────── <# .SYNOPSIS Ensures the Application Insights DLL is loaded and returns a cached TelemetryClient. #> function Initialize-TelemetryClient { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ConnectionString ) if (-not $script:TelemetryDllLoaded) { # Check if the type is already loaded (e.g., from a previous import or direct Add-Type) $typeLoaded = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Microsoft.ApplicationInsights' } if (-not $typeLoaded) { $dllPath = Join-Path $PSScriptRoot 'Telemetry' 'Microsoft.ApplicationInsights.dll' if (-not (Test-Path $dllPath)) { throw "Microsoft.ApplicationInsights.dll not found at '$dllPath'. See README for installation." } Add-Type -Path $dllPath } $script:TelemetryDllLoaded = $true } if ($null -ne $script:TelemetryClient -and $null -ne $script:TelemetryConfig -and $script:TelemetryConfig.ConnectionString -eq $ConnectionString) { return $script:TelemetryClient } if ($null -ne $script:TelemetryClient) { try { $script:TelemetryClient.Flush() } catch { $null = $null } } if ($null -ne $script:TelemetryConfig) { try { $script:TelemetryConfig.Dispose() } catch { $null = $null } } $script:TelemetryConfig = New-Object Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration $script:TelemetryConfig.ConnectionString = $ConnectionString $script:TelemetryClient = New-Object Microsoft.ApplicationInsights.TelemetryClient($script:TelemetryConfig) return $script:TelemetryClient } <# .SYNOPSIS Resolves the Application Insights connection string from multiple sources. .DESCRIPTION Checks sources in priority order: 1. Environment variable: APPLICATIONINSIGHTS_CONNECTION_STRING (Azure standard) 2. Environment variable: EventMonitorAppInsightsConString (custom) 3. File: Telemetry/LogAnalyticsConString.txt Returns $null if no connection string is found anywhere. .OUTPUTS Connection string or $null. #> function Resolve-AppInsightsConnectionString { [CmdletBinding()] param() # Priority 1: Azure standard env var $cs = $env:APPLICATIONINSIGHTS_CONNECTION_STRING if (-not [string]::IsNullOrWhiteSpace($cs)) { Write-EMLog -Message 'Connection string resolved from APPLICATIONINSIGHTS_CONNECTION_STRING env var.' -Level Warning return $cs.Trim() } # Priority 2: Custom env var $cs = $env:EventMonitorAppInsightsConString if (-not [string]::IsNullOrWhiteSpace($cs)) { Write-EMLog -Message 'Connection string resolved from EventMonitorAppInsightsConString env var.' -Level Warning return $cs.Trim() } # Priority 3: File (new standard location) $conStringPath = Join-Path $script:SecretsDir 'ConnectionString.txt' if (Test-Path $conStringPath) { $cs = (Get-Content -Path $conStringPath -Raw).Trim() if (-not [string]::IsNullOrWhiteSpace($cs)) { Write-EMLog -Message 'Connection string resolved from ConnectionString.txt file.' -Level Warning return $cs } } # Priority 4: Legacy file location (backward compat) $legacyPath = Join-Path $PSScriptRoot 'Telemetry' 'LogAnalyticsConString.txt' if (Test-Path $legacyPath) { $cs = (Get-Content -Path $legacyPath -Raw).Trim() if (-not [string]::IsNullOrWhiteSpace($cs)) { Write-EMLog -Message 'Connection string resolved from legacy LogAnalyticsConString.txt.' -Level Warning return $cs } } Write-EMLog -Message 'No App Insights connection string found in env vars or file.' -Level Warning return $null } <# .SYNOPSIS Registers the built-in Application Insights sink. .DESCRIPTION Called automatically when a connection string is provided. You don't need to call this manually unless you removed the sink and want to re-add it. #> function Register-AppInsightsSink { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ConnectionString ) # Store connection string at module scope for the sink callback $script:AppInsightsConnectionString = $ConnectionString Register-TelemetrySink -Name 'AppInsights' -OnDispatch { param($Type, $Name, $Properties, $Metrics, $ErrorRecord) $client = Initialize-TelemetryClient -ConnectionString $script:AppInsightsConnectionString switch ($Type) { 'Event' { $client.TrackEvent($Name, $Properties, $Metrics) } 'Trace' { $client.TrackTrace($Name, $Properties) } 'Exception' { $client.TrackException($ErrorRecord.Exception, $Properties, $Metrics) } } } } # ── Public Dispatch Functions ───────────────────────────────────────────────── # These are the functions that event processors call. They dispatch to ALL # registered sinks. The logAnalyticsConString parameter is kept for backward # compatibility but is optional — if the AppInsights sink is already registered, # it uses the stored connection string. <# .SYNOPSIS Sends a custom event to all registered telemetry sinks. .PARAMETER Name The event name. .PARAMETER Properties Optional string key-value pairs attached to the event. .PARAMETER Metrics Optional numeric key-value pairs attached to the event. #> function TrackEvent { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name, [System.Collections.Generic.Dictionary[string, string]]$Properties, [System.Collections.Generic.Dictionary[string, double]]$Metrics ) try { Invoke-TelemetryDispatch -Type 'Event' -Name $Name -Properties $Properties -Metrics $Metrics } catch { Write-EMLog -Message "TrackEvent dispatch failed for '$Name': $($_.Exception.Message)" -Level Error } } <# .SYNOPSIS Sends a trace message to all registered telemetry sinks. #> function TrackTrace { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [System.Collections.Generic.Dictionary[string, string]]$Properties ) try { Invoke-TelemetryDispatch -Type 'Trace' -Name $Message -Properties $Properties } catch { Write-EMLog -Message "TrackTrace dispatch failed: $($_.Exception.Message)" -Level Error } } <# .SYNOPSIS Sends an exception to all registered telemetry sinks. #> function TrackException { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [System.Collections.Generic.Dictionary[string, string]]$Properties, [System.Collections.Generic.Dictionary[string, double]]$Metrics ) try { Invoke-TelemetryDispatch -Type 'Exception' -Name $ErrorRecord.Exception.Message ` -Properties $Properties -Metrics $Metrics -ErrorRecord $ErrorRecord } catch { Write-EMLog -Message "TrackException dispatch failed: $($_.Exception.Message)" -Level Error } } <# .SYNOPSIS Flushes all telemetry sinks that support flushing. .DESCRIPTION Currently flushes the Application Insights client. Future sinks that buffer data should be flushed here too. #> function Flush-Telemetry { [CmdletBinding()] param() # Flush App Insights if active if ($null -ne $script:TelemetryClient) { try { $script:TelemetryClient.Flush() } catch { Write-EMLog -Message "Flush-Telemetry (AppInsights) failed: $($_.Exception.Message)" -Level Error } } } |