Calling Application Insights API using Powershell

Posted by Rik Hepworth on Saturday, August 6, 2022

If you have an application, instrumenting it with something like Application Insights to emit useful data is something I cannot espouse the benefits of enough. As a service, however, Application Insights can offer other benefits, such as Availability Tests to tell you if the application is accessible to your users.

The default approach to availability tests is to create cloud-based probes that regularly call your application from different regions of the globe to make sure it responds and assess how long those responses take. But what if your application is on premises, and is not accessible from the outside world (or is in the cloud, but secured from general access)?

In this situation, you can create custom availability tests that send data to the Application Insights’ ingestion endpoint. There is a good deal of documentation on how to this in code with the SDK, and even using PowerShell, referencing the .Net assemblies to create the appropriate objects and call methods to transmit data. I really wanted to avoid that if possible, and simply send data directly to the ingestion API.

As with all these things, it’s not well documented…

The frustration of documentation

The SDKs for Application Insights are well documented. The raw APIs are not. However, there is documentation on the basic telemetry objects that at least gives us information on what basic data we need.

For everything else, there is Fiddler!

This post stands on the shoulders of a random thread I found on either the Stack Overflow or Microsoft forums. Annoyingly, I didn’t bookmark it, and in true internet fashion I have never been able to find it since. So whilst I had to repeat the investigative work some kind person had already done, I must give credit where it’s due and note that this path was trodden before me.

In order to see what information we transmit to the ingestion endpoint, I used Fiddler to monitor and inspect the HTTPS traffic that our existing instrumentation sends to the telemetry service. We cover most of the bases - customEvents, customMetrics, traces, requests, and exceptions. However, our existing code does not send availability telemetry.

To figure out the AvailabilityData object I referred to the online documentation for Application Insights’ classes.

Build the PowerShell object and send to AppInsights

The PowerShell script calls a web url and then parses the results in order to send the AvailiabilityData payload to Application Insights.

The code below is the first part of the script, taking parameters for the Application Insights’ connection string, a host header for the site I am calling and an FQDN for the host I want to send the request to.

In our application, we have several websites hosted with IIS that use host headers, and we have several servers, each of which hosts those sites, which are then load balanced. I need to check that each separate instance of each site is functioning. To do that, the code uses invoke-webrequest to call the FQDN provided to direct the request to the right server, adding a host header to ensure we call the correct website.

That call is in a try-catch so that we can grab any exception generated on a failure. I could send that exception to the ingestion endpoint as a separate payload, using the operationId (which we generate as a new GUID for each test) to correlate it to the failed test, but I don’t need that for the job at hand.

We also have a stopwatch which provides us with the duration of the test.

[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [string]
    $ConnectionString,
    [Parameter(Mandatory = $true)]
    [string]
    $HealthCheckTargetHeader,
    [Parameter()]
    [string]
    $HealthCheckTargetFQDN
)

$InstrumentationKey = $ConnectionString.Split(';')[0].split('=')[1]
$IngestionEndpoint = $ConnectionString.Split(';')[1].split('=')[1].trim('/')

if (!$HealthCheckTargetFQDN) {
    $HealthCheckTargetFQDN = $HealthCheckTargetHeader
}

$OperationId = (New-Guid).ToString("N");
$BaseDataSuccess = $false

$Stopwatch = [System.Diagnostics.Stopwatch]::New()
$Stopwatch.Start()

$OriginalErrorActionPreference = $ErrorActionPreference
Try {
    $ErrorActionPreference = "Stop"
    # Run test
    $Response = Invoke-WebRequest -Method "GET" -uri "https://$HealthCheckTargetFQDN" -Headers @{ "Host" = $HealthCheckTargetHeader } -UseBasicParsing
    $Success = $Response.StatusCode -eq 200;
    # End test
    $BaseDataSuccess = $Success
    $BaseDataMessage = 'passed'
}
Catch {
    # Submit Exception details to Application Insights
    $BaseDataMessage = $_.Exception.Message
}
Finally {
    $Stopwatch.Stop()
    $BaseDataDuration = $Stopwatch.ElapsedMilliseconds
    $BaseDataTimestamp = [DateTimeOffset]::UtcNow
    $ErrorActionPreference = $OriginalErrorActionPreference
}

The second part of the script takes the results of the web call and creates a custom powershell object that matches the required object structure, then converts that to json so we get the payload to send, and then invoke-webrequest calls the ingestion URL with that payload.

I’m including some custom properties to help me process the availability data in my own analytics queries. The Tag of ai.cloud.roleInstance ensures that I also have the cloud_roleInstance field in the data which is also useful for cusom queries.

Otherwise, each test needs a name (I use the host header of the site I’m calling), a pass/fail success field, a location for where the test was executed (for the default tests this would be a region but I use the server name which is the same as cloud_roleInstance), the duration of the test (how long did the web call take) and a message (intended to contain the error if the test failed).

$tags = New-Object PSObject
Add-Member -InputObject $tags -NotePropertyName ai.cloud.roleInstance -NotePropertyValue $runLocation

$properties = New-Object PSObject
Add-Member -InputObject $properties -NotePropertyName Target -NotePropertyValue $HostHeader
Add-Member -InputObject $properties -NotePropertyName UrlPath -NotePropertyValue $UrlPath
Add-Member -InputObject $properties -NotePropertyName Host -NotePropertyValue $runLocation
Add-Member -InputObject $properties -NotePropertyName Source -NotePropertyValue $runLocation

$metrics = New-Object PSObject
# Add-Member -InputObject $properties NoteProperty propName 'propValue'

$basedata = New-Object PSObject
Add-Member -InputObject $basedata -NotePropertyName ver -NotePropertyValue 2
Add-Member -InputObject $basedata -NotePropertyName name -NotePropertyValue $Name
Add-Member -InputObject $basedata -NotePropertyName id -NotePropertyValue $OperationId
Add-Member -InputObject $basedata -NotePropertyName runLocation -NotePropertyValue $runLocation
Add-Member -InputObject $basedata -NotePropertyName success -NotePropertyValue $BaseDataSuccess
Add-Member -InputObject $basedata -NotePropertyName message -NotePropertyValue $BaseDataMessage
Add-Member -InputObject $basedata -NotePropertyName duration -NotePropertyValue $BaseDataDuration
Add-Member -InputObject $basedata -NotePropertyName properties -NotePropertyValue $properties
Add-Member -InputObject $basedata -NotePropertyName metrics -NotePropertyValue $metrics

$data = New-Object PSObject
Add-Member -InputObject $data -NotePropertyName baseType -NotePropertyValue 'AvailabilityData'
Add-Member -InputObject $data -NotePropertyName baseData -NotePropertyValue $basedata


$body = New-Object PSObject
Add-Member -InputObject $body -NotePropertyName name -NotePropertyValue 'Microsoft.ApplicationInsights.Event'
Add-Member -InputObject $body -NotePropertyName time -NotePropertyValue $($BaseDataTimestamp.ToString('o'))
Add-Member -InputObject $body -NotePropertyName iKey -NotePropertyValue $InstrumentationKey
Add-Member -InputObject $body -NotePropertyName tags -NotePropertyValue $tags
Add-Member -InputObject $body -NotePropertyName data -NotePropertyValue $data

# Convert the object to json
$sendbody = ConvertTo-Json -InputObject $body -Depth 5

Write-Output "Sending data to ApplicationInsights"
Invoke-WebRequest -Uri "$IngestionEndpoint/v2/track" -Method 'POST' -UseBasicParsing -body $sendbody

Application Insights Payloads

Included below are the other payload types I’ve tried, courtesy of Fiddler and the wider internet.

Event JSON payload

{
  "name": "Microsoft.ApplicationInsights.Event",
  "time": "2022-08-06T00:00:00.0000000Z",
  "iKey": "[MyInstrumentationKey]",
  "tags": {
  },
  "data": {
    "baseType": "EventData",
    "baseData": {
      "ver": 2,
      "name": "SampleEvent",
      "properties": {
        "property1": "value 1",
        "property2": "value 2",
        "property1": "value 3"
      }
    }
  }
}

Message JSON payload

For the payload below, severityLevel is an integer field. The following table shows the integer values and corresponding text labels:

severityLevel Label
0 Verbose
1 Information
2 Warning
3 Error
4 Critical
{
  "name": "Microsoft.ApplicationInsights.Event",
  "time": "2022-08-06T00:00:00.0000000Z",
  "iKey": "[MyInstrumentationKey]",
  "tags":{
  },
  "data": {
    "baseType": "MessageData",
    "baseData": {
      "ver": 2,
      "message": "Simple Trace Log Message",
      "severityLevel": 2,
      "properties": {
        "property1": "value 1",
        "property2": "value 2",
        "property1": "value 3"
      }
    }
  }
}

Metric JSON payload

{
  "name": "Microsoft.ApplicationInsights.Event",
  "time": "2022-08-06T00:00:00.0000000Z",
  "iKey": "[MyInstrumentationKey]",
  "tags": {
  },
  "data": {
    "baseType": "MetricData",
    "baseData": {
      "ver": 2,
      "metrics": [
        {
          "name": "BasicMetric",
          "kind": "Measurement",
          "value": 42
        }
      ],
      "properties": {
        "property1": "value 1",
        "property2": "value 2",
        "property1": "value 3"
      }
    }
  }
 }

Exception JSON payload

{
 "name": "Microsoft.ApplicationInsights.Event",
 "time": "2022-08-06T00:00:00.0000000Z",
 "iKey": "[MyInstrumentationKey]",
 "tags": {
 },
 "data": {
   "baseType": "ExceptionData",
   "baseData": {
     "ver": 2,
     "handledAt": "UserCode",
     "properties": {
       "property1": "value 1",
       "property2": "value 2",
       "property1": "value 3"
     },
     "exceptions": [
       {
         "id": 12345678,
         "typeName": "System.Exception",
         "message": "My exception message",
         "hasFullStack": true,
         "parsedStack": [
           {
             "level": 0,
             "method": "Console.Program.Main",
             "assembly": "Console, Version=1.0",
             "fileName": "/MyApp/program.cs",
             "line": 1
           }
         ]
       }
     ]
   }
 }
}

AvailabilityTest JSON payload

{
  "name": "Microsoft.ApplicationInsights.Availability",
  "time": "2022-08-06T00:00:00.0000000Z",
  "iKey": "[MyInstrumentationKey]",
  "tags": {
  },
  "data": {
    "baseType": "AvailabilityData",
    "baseData": {
      "ver": 2,
      "name": "SampleAvailability",
      "duration": "timespan",
      "runlocation": "UK",
      "success": true,
      "message": "error message",
      "properties": {
        "property1": "value 1",
        "property2": "value 2",
        "property1": "value 3"
      },
      "metrics": [
        {
        "name": "BasicMetric",
        "kind": "Measurement",
        "value": 42
        }
      ]
    }
  }
}