PSAppInsightsFix.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
<#
    PowerShell App Insights Module
    V0.9.1
    Application Insight Tracing to Powershell Scripts and Modules
 
Documentation :
    Ref .Net : https://msdn.microsoft.com/en-us/library/microsoft.applicationinsights.aspx
    Ref JS : https://github.com/Microsoft/ApplicationInsights-JS/blob/master/API-reference.md
#>


if ( $Global:AISingleton -eq $null ) {

#Only initialize on the first load of the module
    $Global:AISingleton = @{
        ErrNoClient = "Client - No Application Insights Client specified or initialized."
        Configuration = $null    
        #The current AI Client
        Client = $null
        #The Perfmon Collector
        PerformanceCollector = $null
        #QuickPulse aka Live Metrics Stream
        QuickPulse = $null
        #Stack of current Operations
        Operations = [System.Collections.Stack]::new()
    }
} 
<#
.Synopsis
   Start a new Application Insights Client to Log events and timings to AI
.DESCRIPTION
   Long description
.EXAMPLE
   $C1 = New-AIClient -InstrumentationKey $key
 
#>

function New-AIClient
{
    [CmdletBinding()]
    [Alias('New-AISession')]
    [OutputType([Microsoft.ApplicationInsights.TelemetryClient])]
    Param
    (
        # The Instrumentation Key for Application Analytics
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [Alias("Key")]
        [String]$InstrumentationKey,
        #An Identifier to use for this session
        [string]$SessionID = ( New-Guid), 
        #Operation, by default the Scriptname will be used
        [string]$OperationID , # Use the scriptname if it can be found
        #Version of the application or Component, defaults to retrieving theversion from the script
        $Version,
        # Set to indicate messages sent from or during a test
        [string]$Synthetic = $null,

        #Set of initializers - Default: Operation Correlation is enabled
        [Alias("Init")]
        [ValidateSet('Domain','Device','Operation','Dependency')]
        [String[]] $Initializer = @(), 
        
        #Allow Personal Identifiable information such as Computernames and current user name to be sent in the Traces
        [Alias("PII")]
        [switch]$AllowPII,

        #Send through Fiddler for debugging
        [switch]$Fiddler,
        
        #When developer mode is True, sends telemetry to Application Insights immediately during the entire lifetime of the application
        [switch]$DeveloperMode,

        # Sets the maximum telemetry batching interval in seconds. Once the interval expires, sends the accumulated telemetry items for transmission.
        [ValidateRange(1, 1440)] #Up to day should be sufficient
        $SendingInterval = 0 
    )

    Process
    {
        if ( [String]::IsNullOrEmpty($OperationID) ){
        #Find a sensible toplevel Operation ID
            #Get the topmost caller's information
            $TopInfo = getCallerInfo -level (Get-PSCallStack).Length
            if     ($TopInfo.Script)                      { $OperationID = $TopInfo.Script } 
            else { 
                if ($TopInfo.Command -ne '<ScriptBlock>') { $OperationID = $TopInfo.Command } 
                else { 
                    if ($TopInfo.FunctionName -ne '<ScriptBlock>') { $OperationID = $TopInfo.FunctionName }
                    # Otherwise AI will set a GUID
                }
            }
        }
        
        try { 
            Write-Verbose "create Telemetry client"
            # This is a singleton that controls all New AI Client sessions for this process from this moment
            [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.InstrumentationKey = $InstrumentationKey
            [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.DisableTelemetry = $false

            #optionally add Fiddler for debugging
            if ($fiddler) { 
                [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.TelemetryChannel.EndpointAddress = 'http://localhost:8888/v2/track'
            }

            #Activate/deactivate developermode
            if ($DeveloperMode) {
                Write-Verbose "Set DeveloperMode" 
                [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.TelemetryChannel.DeveloperMode = $true
            } else {
                Write-Verbose "Set DeveloperMode off" 
                [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.TelemetryChannel.DeveloperMode = $false
            }

            If ($SendingInterval -ne 0)
            {        
                Write-Verbose "Set Bufferdelay to $SendingInterval seconds." 
                [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.TelemetryChannel.SendingInterval = New-TimeSpan -Seconds $SendingInterval
            }

            $Global:AISingleton.Configuration = [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active
            #----------------------
            # Initialisers
            # - A context initializer will only be called once per TelemetryClient instance
            # - A Telemetry Initialiser will be called for each Telemetry 'Message'
            # ---------------------
            # ITelemetryProcessor and ITelemetryInitializer
            # What's the difference between telemetry processors and telemetry initializers?
            # There are some overlaps in what you can do with them: both can be used to add properties to telemetry.
            # - TelemetryInitializers always run before TelemetryProcessors.
            # - TelemetryProcessors allow you to completely replace or discard a telemetry item.
            # - TelemetryProcessors don't process performance counter telemetry
            #----------------------
            #Context Initialisers
            #----------------------
            #Add domain initialiser to add domain and machine info
            if ($Initializer.Contains('Domain')) {
                Try { 
                    Write-Verbose "Add initializer- domain and machine info" 
                    $DomInit = [Microsoft.ApplicationInsights.WindowsServer.DomainNameRoleInstanceTelemetryInitializer]::new()
                    $Global:AISingleton.Configuration.TelemetryInitializers.Add($DomInit)
                } catch { 
                    #Warn but do not abort
                    Write-Warning "Could not add the Domain initialiser"
                }
            }
            #Add device initialiser to add client info
            if ($Initializer.Contains('Device')) {
                
                #If on AzureAutomation, just report Azure automation
                If ($false) {
                    Write-Verbose "TODO Add Azure Automation- device info"
                    
                }else {
                    Try { 
                        Write-Verbose "Add initializer- device info"
                        $DeviceInit = [Microsoft.ApplicationInsights.WindowsServer.DeviceTelemetryInitializer]::new()
                        $Global:AISingleton.Configuration.TelemetryInitializers.Add($DeviceInit)
                    } catch { 
                        #Warn but do not abort
                        Write-Warning "Could not add the Device initialiser"
                    }

                }
            }
            #----------------------
            #Telemetry Initialisers
            #----------------------
            # Add the initialisers specified
            # If you provide a telemetry initializer, it is called whenever any of the Track*() (ai native) methods is called.
            # This includes methods called by the standard telemetry modules. By convention, these modules
            # do not set any property that has already been set by an initializer

            if ($Initializer.Contains('Operation')) {
                Try { 
                    Write-Verbose "Add initializer- operation correlation" 
                    $OpInit = [Microsoft.ApplicationInsights.Extensibility.OperationCorrelationTelemetryInitializer]::new()
                    $Global:AISingleton.Configuration.TelemetryInitializers.Add($OpInit)
                } catch { 
                    #Warn but do not abort
                    Write-Warning "Could not add the Operation initialiser"
                }
            }


            #Add dependency collector to (automatically ?) measure dependencies
            if ($Initializer.Contains('Dependency')) {
                Try { 
                    Write-Verbose "Add initializer- dependency collector"
                    $Dependency = [Microsoft.ApplicationInsights.DependencyCollector.DependencyTrackingTelemetryModule]::new();
                    $TelemetryModules = [Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryModules]::Instance;
                    $TelemetryModules.Modules.Add($Dependency);
                } catch { 
                    #Warn but do not abort
                    Write-Warning "Could not add the Dependency initialiser"
                }

            }

            # Send any unhandled exceptions
            # The module subscribed to AppDomain.CurrentDomain.UnhandledException to send exceptions to ApplicationInsights.

<#
            $Unhandled = [Microsoft.ApplicationInsights.WindowsServer.UnhandledExceptionTelemetryModule]::New()
                $TelemetryModules = [Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryModules]::Instance;
                $TelemetryModules.Modules.Add($Unhandled);
#>

            #Now that they are added, they still need to be initialised
            # Telemetry modules first
            if ($Global:AISingleton.Configuration.TelemetryInitializers) {
                Write-Verbose "Initialize- Telemetry modules"
                $Global:AISingleton.Configuration.TelemetryInitializers | 
                    Where-Object {$_ -is 'Microsoft.ApplicationInsights.Extensibility.ITelemetryModule'} |
                    ForEach-Object { 
                        Try { 
                            $_.Initialize($Global:AISingleton.Configuration); 
                            Write-Verbose ".."
                        } catch { 
                            Write-Warning 'Error during initialisation of Telemetry Module'
                        }
                    }
            }
            #Then the telemetry processors
            if ($Global:AISingleton.Configuration.TelemetryProcessorChain.TelemetryProcessors ) { 
                Write-Verbose "Initialize- Telemetry Processors"
                $Global:AISingleton.Configuration.TelemetryProcessorChain.TelemetryProcessors |
                    Where-Object {$_ -is 'Microsoft.ApplicationInsights.Extensibility.ITelemetryModule'} |
                    ForEach-Object { 
                        Try { 
                            $_.Initialize($Global:AISingleton.Configuration); 
                            Write-Verbose ".."
                        } catch { 
                            Write-Warning 'Error during initialisation of Telemetry Module'
                        } 
                    }
            } 
            #Now get the initialised modules
            $TelemetryModules = [Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryModules]::Instance;
            
            # todo: check if the 2nd initialisation is really needed
            $TelemetryModules.Modules | 
                Where-Object {$_ -is 'Microsoft.ApplicationInsights.Extensibility.ITelemetryModule'} |
                ForEach-Object { $_.Initialize($Global:AISingleton.Configuration); }
            
            #Time to start the client
            $client = [Microsoft.ApplicationInsights.TelemetryClient]::new($Global:AISingleton.Configuration)

            if ($client) { 
                Write-Verbose "Add Key, Session.id and Operation.id"
                
                $client.InstrumentationKey = $InstrumentationKey
                $client.Context.Session.Id = $SessionID
                #Operation : A generated value that correlates different events, so that you can find "Related items"
                $client.Context.Operation.Id = $OperationID

                #do some standard init on the context
                # set properties such as TelemetryClient.Context.User.Id to track users and sessions,
                # or TelemetryClient.Context.Device.Id to identify the machine.
                # This information is attached to all events sent by the instance.
                
                if($PSPrivateMetadata.JobId) {
                    # in Azure Automation
                    Write-Verbose "Add Azure Automation and JobID"
                    $client.Context.Cloud.RoleName = 'Azure Automation'
                    $client.Context.Cloud.RoleInstance = $PSPrivateMetadata.JobId

                    $client.Context.Device.OperatingSystem = 'Azure Automation'
                }
                else {
                   # not in Azure Automation
                    Write-Verbose "Add device.OS and User Agent"
                    #OS cannot be read in Azure automation, handle gracefully

                    $OS = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction SilentlyContinue
                    if ($OS) {
                        $client.Context.Device.OperatingSystem = $OS.version
                    }
                }
                $client.Context.User.UserAgent = $Host.Name

                if ($AllowPII) {
                    Write-Verbose "Add PII user and computer information"

                    #Only if Explicitly noted
                    $client.Context.Device.Id = $env:COMPUTERNAME 
                    $client.Context.User.Id = $env:USERNAME 
                } else { 
                    Write-Verbose "Add NON-PII user and computer identifiers"
                    #Default to NON-PII
                    $client.Context.Device.Id = (Get-StringHash -String $env:COMPUTERNAME -HashType MD5).hash 
                    $client.Context.User.Id = (Get-StringHash -String $env:USERNAME -HashType MD5).hash  
                }
                if ($Global:AISingleton.Client -ne $null ) {
                    Write-Verbose "Replacing current active telemetry client, with the new Telemetry client"
                    Flush-AIClient -Client $Global:AISingleton.Client
                    $Global:AISingleton.Client = $null
                } 
                #Save client in Global for re-use when not specified
                $Global:AISingleton.Client = $client

                if ([string]::IsNullOrEmpty($Version) ) {
                    write-verbose "retrieve version of calling script or module."
                    $Version = getCallerVersion 
                } 
                write-verbose "use version $([string]$version)"
                $client.Context.Component.Version = [string]($version)
                

                #Indicate actual / Synthethic events
                $Global:AISingleton.Client.Context.Operation.SyntheticSource = $Synthetic

                return $client 
            } else { 
                Throw "Could not create ApplicationInsights Client.."
            }
        } catch {
            Throw "Could not create ApplicationInsights Client."
        }
    }
}


<#
.Synopsis
    Flush the Application Insights Queue to the AI Service
    Forces the sending of any remaining messages in the send queue
#>

function Push-AIClient
{
    [CmdletBinding()]
    [Alias("Flush-AIClient")]
    [Alias("Flush-AISession")]  # Depricated

    Param
    (
        #The AppInsights Telemetry client object to use (Default from singleton).
        [Parameter(Mandatory=$false)]
        [Microsoft.ApplicationInsights.TelemetryClient] $Client 
    )
    #Check for a specified AI client
    if ($Client -eq $null) {
        If ( ($Global:AISingleton ) -AND ( $Global:AISingleton.Client ) ) {
            #Use Current Client
            $Client = $Global:AISingleton.Client
        }
    }
    if ($Client) { 
        $client.Flush()
    } else {
        Write-Verbose 'No Client initialised'
    }
}

<#
.Synopsis
    Stop and flush the App Insights telemetry client, and disables the per-process config.
.EXAMPLE
    Stop-AIClient
.EXAMPLE
    Stop-AIClient -Client $TelemetryClient
#>

function Stop-AIClient
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        # The Telemetry client to flush and stop, defaults to the
        [Parameter(Mandatory=$false)]
        $Client = $Global:AISingleton.Client
    )
    if ($Client) {
        Write-Verbose "Stopping telemetry client"
        Flush-AIClient -Client $Client
        #And disable telemetry for
        [Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration]::Active.DisableTelemetry = $true
    } else {
        Write-Warning "No AppInsights telemetry client active"
    }
}