diff --git a/PSApplicationInsights/PSApplicationInsights/PSApplicationInsights.psd1 b/PSApplicationInsights/PSApplicationInsights/PSApplicationInsights.psd1 new file mode 100644 index 0000000..0d7aeaa --- /dev/null +++ b/PSApplicationInsights/PSApplicationInsights/PSApplicationInsights.psd1 @@ -0,0 +1,79 @@ +@{ + # Root Module + RootModule = 'PSApplicationInsights.psm1' + + # Version Number + ModuleVersion = '1.0' + + # Unique Module ID + GUID = '3ea2fc69-50e2-4c25-95ae-89164ef9b388' + + # Module Author + Author = 'Antonio Blanco' + + # Company + CompanyName = '' + + # Copyright + Copyright = '' + + # Module Description + Description = 'PowerShell wrapper for Microsoft Application Insights' + + # Minimum PowerShell Version Required + PowerShellVersion = '2.0' + + # Name of Required PowerShell Host + #PowerShellHostName = '' + + # Minimum Host Version Required + #PowerShellHostVersion = '' + + # Minimum .NET Framework-Version + DotNetFrameworkVersion = '4.5' + + # Minimum CLR (Common Language Runtime) Version + #CLRVersion = '' + + # Processor Architecture Required (X86, Amd64, IA64) + #ProcessorArchitecture = '' + + # Required Modules (will load before this module loads) + RequiredModules = @() + + # Required Assemblies + RequiredAssemblies = @() + + # PowerShell Scripts (.ps1) that need to be executed before this module loads + ScriptsToProcess = @() + + # Type files (.ps1xml) that need to be loaded when this module loads + TypesToProcess = @() + + # Format files (.ps1xml) that need to be loaded when this module loads + FormatsToProcess = @() + + # + NestedModules = @() + + # List of exportable functions + FunctionsToExport = @('New-TelemetryClient') + + # List of exportable cmdlets + #CmdletsToExport = '*' + + # List of exportable variables + #VariablesToExport = '*' + + # List of exportable aliases + #AliasesToExport = '*' + + # List of all modules contained in this module + #ModuleList = @() + + # List of all files contained in this module + #FileList = @() + + # Private data that needs to be passed to this module + #PrivateData = '' +} \ No newline at end of file diff --git a/PSApplicationInsights/PSApplicationInsights/PSApplicationInsights.psm1 b/PSApplicationInsights/PSApplicationInsights/PSApplicationInsights.psm1 new file mode 100644 index 0000000..eb0ddf4 --- /dev/null +++ b/PSApplicationInsights/PSApplicationInsights/PSApplicationInsights.psm1 @@ -0,0 +1,296 @@ +function ConvertTo-Dictionary +{ + #requires -Version 2.0 + + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $InputObject, + [Parameter(Mandatory = $true)] + [Type]$ValueType + ) + + process + { + $outputObject = New-Object -TypeName "System.Collections.Generic.Dictionary[[string],[$($ValueType.FullName)]]]" + + foreach ($entry in $InputObject.GetEnumerator()) + { + $newKey = $entry.Key -as [string] + + if ($null -eq $newKey) + { + $message = 'Could not convert key "{0}" of type "{1}" to type "{2}"'-f + $entry.Key, + $entry.Key.GetType().FullName, + [string].FullName + + Write-Warning -Message $message + } + elseif ($outputObject.ContainsKey($newKey)) + { + Write-Warning -Message "Duplicate key `"$newKey`" detected in input object." + } + + $outputObject.Add($newKey, $entry.Value) + } + + Write-Output -InputObject $outputObject + } +} + +function New-TrackEventOverload +{ + param( + [Parameter(Mandatory=$true)] + $TrackEventDelegate + ) + + # When we add a script method, the scope becomes the current user scope instead of the module scope + # Because of this, our private function ConvertTo-Dictionary is not available in the user scope. + + # To avoid this, we can create a closure over the local variable which has the function definition + $convertToDictionaryFunc = ${function:ConvertTo-Dictionary} + return { + param( + [Parameter(Mandatory=$true)] + [string] $EventName, + [AllowNull()] + [hashtable] $Properties, + [AllowNull()] + [hashtable] $Metrics + ) + + $propertiesDictionary = $Properties | & $convertToDictionaryFunc -ValueType string + $metricsDictionary = $Metrics | & $convertToDictionaryFunc -ValueType double + + $TrackEventDelegate.Invoke($EventName, $propertiesDictionary, $metricsDictionary) + }.GetNewClosure() +} + +function New-TrackMetricOverload +{ + param( + [Parameter(Mandatory=$true)] + $TrackMetricDelegate + ) + + # When we add a script method, the scope becomes the current user scope instead of the module scope + # Because of this, our private function ConvertTo-Dictionary is not available in the user scope. + + # To avoid this, we can create a closure over the local variable which has the function definition + $convertToDictionaryFunc = ${function:ConvertTo-Dictionary} + return { + param( + [Parameter(Mandatory=$true)] + [string] $MetricName, + [Parameter(Mandatory=$true)] + [double] $Value, + [AllowNull()] + [hashtable] $Properties + ) + $propertiesDictionary = $Properties | & $convertToDictionaryFunc -ValueType string + + $TrackMetricDelegate.Invoke($MetricName, $Value, $propertiesDictionary) + }.GetNewClosure() +} + +function New-TrackTraceOverload +{ + param( + [Parameter(Mandatory=$true)] + $TrackTraceDelegate + ) + + # When we add a script method, the scope becomes the current user scope instead of the module scope + # Because of this, our private function ConvertTo-Dictionary is not available in the user scope. + + # To avoid this, we can create a closure over the local variable which has the function definition + $convertToDictionaryFunc = ${function:ConvertTo-Dictionary} + return { + param( + [Parameter(Mandatory=$true)] + [string] $Message, + [AllowNull()] + [string] $SeverityLevel, + [AllowNull()] + [hashtable] $Properties + ) + $propertiesDictionary = $Properties | & $convertToDictionaryFunc -ValueType string + + try { + $level = [Microsoft.ApplicationInsights.DataContracts.SeverityLevel]($SeverityLevel) + $TrackTraceDelegate.Invoke($Message, $level, $propertiesDictionary) + } + catch { + Write-Warning -Message "Application Insights: No valid Severity Level was specified, using the default instead. Valid severity levels are 'Verbose','Information','Warning','Error','Critical'" + + $TrackTraceDelegate.Invoke($Message, $propertiesDictionary) + } + }.GetNewClosure() +} + +function New-TrackExceptionOverload +{ + param( + [Parameter(Mandatory=$true)] + $TrackExceptionDelegate + ) + + # When we add a script method, the scope becomes the current user scope instead of the module scope + # Because of this, our private function ConvertTo-Dictionary is not available in the user scope. + + # To avoid this, we can create a closure over the local variable which has the function definition + $convertToDictionaryFunc = ${function:ConvertTo-Dictionary} + return { + param( + [Parameter(Mandatory=$true)] + [Exception] $Exception, + [AllowNull()] + [hashtable] $Properties, + [AllowNull()] + [hashtable] $Metrics + ) + $propertiesDictionary = $Properties | & $convertToDictionaryFunc -ValueType string + $metricsDictionary = $Metrics | & $convertToDictionaryFunc -ValueType double + + $TrackExceptionDelegate.Invoke($Exception, $propertiesDictionary, $metricsDictionary) + }.GetNewClosure() +} + +function New-TelemetryClient +{ + <# + .SYNOPSIS + Creates a new instance of an Application Insights TelemetryClient + + .DESCRIPTION + Creates a new instance of an Application Insights TelemetryClient that can be used in your PowerShell scripts. + Includes PowerShell overloads for using Hashtables for adding custom properties and custom metrics. + Because this instance uses the InMemory channel, be sure to call the Flush() method to flush and send telemetry to App Insights. + + .PARAMETER InstrumentationKey + The Application Insights Instrumentation Key + + .EXAMPLE + Create a new telemetry client instance + $ai = New-TelemetryClient -InstrumentationKey "iKey" + + .EXAMPLE + Track a event with custom properties + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackEvent('event') + + .EXAMPLE + Track a event with custom properties + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackEvent('event', @{customProperty='a'}) + + .EXAMPLE + Track a event with custom properties and custom metrics + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackEvent('event', @{customProperty='a'}, @{customMetric=3.0}) + + .EXAMPLE + Track a metric with custom properties + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackMetric('metric', 1) + + .EXAMPLE + Track a metric with custom properties + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackMetric('metric', 1, @{customProperty='a'}) + + .EXAMPLE + Track a metric with custom properties and custom metrics + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackMetric('metric', 1, @{customProperty='a'}, @{customMetric=3.0}) + + .EXAMPLE + Track a trace + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackTrace('trace') + + .EXAMPLE + Track a trace with a severity level. Valid severity levels are 'Verbose','Information','Warning','Error','Critical' + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackTrace('trace', 'verbose') + + .EXAMPLE + Track a trace with a severity level and custom properties + $ai = New-TelemetryClient -InstrumentationKey "iKey" + $ai.TrackTrace('trace', 'verbose', @{customProperty='a'}) + + .EXAMPLE + Track a exception + $ai = New-TelemetryClient -InstrumentationKey "iKey" + try{ + throw 'exception' + } + catch { + $exception = $_.Exception + $tc.TrackException($exception) + } + + .EXAMPLE + Track a exception with custom properties + $ai = New-TelemetryClient -InstrumentationKey "iKey" + try{ + throw 'exception' + } + catch { + $exception = $_.Exception + $tc.TrackException($exception, @{customProperty='PowerShell!'}) + } + + .EXAMPLE + Track a exception with custom properties and custom metrics + $ai = New-TelemetryClient -InstrumentationKey "iKey" + try{ + throw 'exception' + } + catch { + $exception = $_.Exception + $tc.TrackException($exception, @{'customProperty'='PowerShell!'}, @{customMetrics=3.0}) + } + + .NOTES + Author: Antonio Blanco (anblanco@microsoft.com) + + .LINK + https://docs.microsoft.com/en-us/azure/application-insights/ + + .INPUTS + [System.String] + + .OUTPUTS + [Microsoft.ApplicationInsights.TelemetryClient] + #> + + param( + [Parameter(Mandatory=$true)] + [string] $InstrumentationKey + ) + + Add-Type -Path "$PSScriptRoot\microsoft.applicationinsights.2.2.0-beta4\lib\net45\Microsoft.ApplicationInsights.dll" + $appInsights = New-Object -TypeName Microsoft.ApplicationInsights.TelemetryClient + $appInsights.InstrumentationKey = $InstrumentationKey + + #TrackEvent overload + $trackEvent = New-TrackEventOverload -TrackEventDelegate $appInsights.TrackEvent + $appInsights | Add-Member -MemberType ScriptMethod -Name TrackEvent -Value $trackEvent -Force + + #TrackMetric overload + $trackMetric = New-TrackMetricOverload -TrackMetricDelegate $appInsights.TrackMetric + $appInsights | Add-Member -MemberType ScriptMethod -Name TrackMetric -Value $trackMetric -Force + + #TrackTrace overload + $trackTrace = New-TrackTraceOverload -TrackTraceDelegate $appInsights.TrackTrace + $appInsights | Add-Member -MemberType ScriptMethod -Name TrackTrace -Value $trackTrace -Force + + #TrackException overload + $trackException = New-TrackExceptionOverload -TrackExceptionDelegate $appInsights.TrackException + $appInsights | Add-Member -MemberType ScriptMethod -Name TrackException -Value $trackException -Force + + Write-Output -InputObject $appInsights +} \ No newline at end of file diff --git a/PSApplicationInsights/PSApplicationInsights/en-US/about_PSApplicationInsights.help.txt b/PSApplicationInsights/PSApplicationInsights/en-US/about_PSApplicationInsights.help.txt new file mode 100644 index 0000000..3eaa90f --- /dev/null +++ b/PSApplicationInsights/PSApplicationInsights/en-US/about_PSApplicationInsights.help.txt @@ -0,0 +1,26 @@ +PSTOPIC + about_PSApplicationInsights + +SHORT DESCRIPTION + PSApplicationInsights is a module that provides PowerShell friendly overloads for the core Application Insights SDK. + +LONG DESCRIPTION + PSApplicationInsights provides a new PowerShell cmdlet, New-TelemetryClient, and overloads existing Track* calls + to support Hashtables which have a more natual syntax in PowerShell. It is packaged as a single module with the latest + version of Application Insights and has working examples for all of the Track* calls. + +DETAILED DESCRIPTION + PSApplicationInsights was written with two scenarios in mind + 1) Allowing a PowerShell developer to more easily integrate PowerShell scripts and telemetry to existing apps + + Example: A web app hosted in Azure with a SQL Azure backend is likely to have existing web app telemetry. + This app may perform common automation tasks such as invoking a webhook as an alert rule, or scheduled scale up and + scale down of the SQL Azure database. Using PSApplicationInsights one can quick add telemetry to these automation tasks + from their scripts or runbooks, such as: + TrackEvent('Sql Resize'. @{databaseName=$databaseName}, @{resizeTime=$duration.Seconds}). + + 2) Allowing PowerShell script authors to more easily instrument their scripts to track script and feature usage + + Example: Wouldn't it be nice to know what cmdlets get used the most? Or if users are actually using that + alternate parameter set you defined? PSApplicationInsights makes it easy and natural to instrument your scripts + using the Application Insights SDK \ No newline at end of file diff --git a/PSApplicationInsights/README.md b/PSApplicationInsights/README.md new file mode 100644 index 0000000..1efdd1d Binary files /dev/null and b/PSApplicationInsights/README.md differ diff --git a/PSApplicationInsights/Tests/PSApplicationInsights.tests.ps1 b/PSApplicationInsights/Tests/PSApplicationInsights.tests.ps1 new file mode 100644 index 0000000..9350e4d --- /dev/null +++ b/PSApplicationInsights/Tests/PSApplicationInsights.tests.ps1 @@ -0,0 +1,106 @@ +Import-Module $PSScriptRoot\..\PSApplicationInsights -Force + +Describe 'New-TelemetryClient' { + + Context 'Constructor' { + + It 'does not throw' { + { New-TelemetryClient -InstrumentationKey "iKey" } | Should Not Throw + } + + It 'returns a new telemetry client' { + New-TelemetryClient -InstrumentationKey "iKey" | Should BeOfType Microsoft.ApplicationInsights.TelemetryClient + } + + It 'sets the instrumentation key' { + $sut = New-TelemetryClient -InstrumentationKey "iKey" + $sut.InstrumentationKey | Should Be "iKey" + } + } + + Context 'TrackEvent' { + BeforeEach {$sut = New-TelemetryClient -InstrumentationKey "iKey" } + + It 'tracks event with only event name' { + { $sut.TrackEvent('event',@{customProperty='a'}) } | Should Not Throw + } + It 'tracks event with custom properties' { + { $sut.TrackEvent('event',@{customProperty='a'}) } | Should Not Throw + } + It 'tracks event with custom properties and metrics' { + { $sut.TrackEvent('event',@{customProperty='a'}, @{customMetric=3.0}) } | Should Not Throw + } + } + + Context 'TrackMetric' { + BeforeEach {$sut = New-TelemetryClient -InstrumentationKey "iKey" } + + It 'tracks metric with only metric name' { + { $sut.TrackMetric('metric',1,@{customProperty='a'}) } | Should Not Throw + } + It 'tracks metric with custom properties' { + { $sut.TrackMetric('metric',1,@{customProperty='a'}) } | Should Not Throw + } + It 'tracks metric with custom properties and metrics' { + { $sut.TrackMetric('metric',1,@{customProperty='a'}, @{customMetric=3.0}) } | Should Not Throw + } + } + + Context 'TrackTrace' { + BeforeEach {$sut = New-TelemetryClient -InstrumentationKey "iKey" } + + It 'tracks traces ' { + { $sut.TrackTrace('trace') } | Should Not Throw + } + + It 'tracks traces with severity level' { + { $sut.TrackTrace('trace', 'verbose') } | Should Not Throw + } + It 'tracks traces with severity level, and custom properties' { + { + $sut.TrackTrace('trace', 'Verbose', @{customProperty='PowerShell!'} + ) + } | Should Not Throw + } + It 'tracks traces with severity level, custom properties and custom metrics' { + { + $sut.TrackTrace('trace', + 'verbose', + @{'customProperty'='PowerShell!'}, + @{customMetrics=3.0} + ) + } | Should Not Throw + } + + It 'does not throw if it traces with invalid severity because core sdk never throws' { + { $sut.TrackTrace('trace', 'invalid') } | Should Not Throw + } + } + + Context 'TrackException' { + BeforeEach { + $sut = New-TelemetryClient -InstrumentationKey "iKey" + try{ throw 'exception'} catch { $exception = $_.Exception} + } + + It 'tracks exceptions' { + { $sut.TrackException($exception) } | Should Not Throw + } + + It 'tracks exceptions with custom properties' { + { + $sut.TrackException($exception, + @{customProperty='PowerShell!'} + ) + } | Should Not Throw + } + It 'tracks exceptions with custom properties and custom metrics' { + { + $sut.TrackException($exception, + @{'customProperty'='PowerShell!'}, + @{customMetrics=3.0} + ) + } | Should Not Throw + } + } +} \ No newline at end of file diff --git a/PSApplicationInsights/build.ps1 b/PSApplicationInsights/build.ps1 new file mode 100644 index 0000000..db22f96 --- /dev/null +++ b/PSApplicationInsights/build.ps1 @@ -0,0 +1,13 @@ +# restore NuGet packages +. $PSScriptRoot\..\NuGet.exe install $PSScriptRoot\packages.config -OutputDirectory $PSScriptRoot\PSApplicationInsights\ + +# run tests +Import-Module Pester +$testResults = Invoke-Pester -Script $PSScriptRoot\Tests\PSApplicationInsights.Tests.ps1 -PassThru + +if ($testResults.FailedCount -gt 0) +{ + throw "BUILD FAILED: There were $($testResults.FailedCount ) failed tests... stopping" +} + +# deploy? \ No newline at end of file diff --git a/PSApplicationInsights/examples/PSApplicationInsightsExamples.ps1 b/PSApplicationInsights/examples/PSApplicationInsightsExamples.ps1 new file mode 100644 index 0000000..0b7e16f --- /dev/null +++ b/PSApplicationInsights/examples/PSApplicationInsightsExamples.ps1 @@ -0,0 +1,82 @@ +<# +# old way +Add-Type -Path "microsoft.applicationinsights.2.0.0-rc1\lib\net45\Microsoft.ApplicationInsights.dll" +$tc = New-Object -TypeName Microsoft.ApplicationInsights.TelemetryClient +$tc.InstrumentationKey = "iKey" +#> + +# PSApplicationInsights +$tc = New-TelemetryClient -InstrumentationKey "iKey" + +# Events + +$tc.TrackEvent('event') +$tc.TrackEvent('event', @{customProperty='PowerShell!'}) +$tc.TrackEvent('event', @{customProperty='PowerShell!'}, @{customMetrics=3.0}) + +# Metrics + +$tc.TrackMetric('metric', 1) +$tc.TrackMetric('metric', 1, @{customProperty='PowerShell!'}) +$tc.TrackMetric('metric', 1, @{customProperty='PowerShell!'}, @{customMetrics=3.0}) + +# Traces + +$tc.TrackTrace('trace') +$tc.TrackTrace('trace', 'Verbose') +$tc.TrackTrace('trace', 'Verbose', @{customProperty='PowerShell!'}) +$tc.TrackTrace('trace', 'Verbose', @{customProperty='PowerShell!'}, @{customMetrics=3.0}) + +# Exceptions + +try{ + throw 'exception' +} +catch { + $exception = $_.Exception + + $tc.TrackException($exception) + $tc.TrackException($exception, @{customProperty='PowerShell!'}) + $tc.TrackException($exception, @{customProperty='PowerShell!'}, @{customMetrics=3.0}) +} + +# Availability + +function Get-Availability +{ + $true # or custom logic / Invoke-RestMethod +} + +$duration = Measure-Command { + [bool] $isSuccess = Get-Availability +} + +if ($isSuccess) { + $tc.TrackAvailability('test',(Get-Date), $duration, 'run from powershell!', $isSuccess) +} +else { + $tc.TrackAvailability('test',(Get-Date), $duration, 'run from powershell!', $false, 'more detailed error message') +} + +# Dependency + +function Get-Dependency +{ + $true # or custom logic / Invoke-RestMethod +} + +$duration = Measure-Command { + [bool] $result = Get-Dependency +} + +$tc.TrackDependency('dependency name', 'command name', (Get-Date), $duration, $result) +$tc.TrackDependency('dependency type name', 'target', 'dependency name', 'data', (Get-Date), $duration, '200', $result) + +# Page View +$tc.TrackPageView('page name') + +# Request +$tc.TrackRequest('name', (Get-Date), $duration, '200', $true) + +# Flush +$tc.Flush() \ No newline at end of file diff --git a/PSApplicationInsights/packages.config b/PSApplicationInsights/packages.config new file mode 100644 index 0000000..9b2d333 --- /dev/null +++ b/PSApplicationInsights/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file