Azure Metrics in Teams with Azure Functions and Adaptive Card Tables

John Kilmister, ยท 7 min read

Azure has many useful metrics, and it is possible to set up email alerts and dashboards for these, however, these can get missed. In this article, I will show you how to create an Azure Function that will post a message to a Microsoft Teams channel with a table of metrics once a day.

I have added the code for this post on GitHub for reference.

Design

We will use a C# Azure Function to retrieve the Azure metrics for a virtual machine and post them to Microsoft Teams. The function will be triggered on a timer and will use the Azure Monitor API to get the metrics. Once we have the metrics we will finally bind the data to a message that we will then post to Microsoft Teams.

Although the code is in C#, and focuses on VM metrics you should be able to use the same principles to get metrics for other resources and use other languages.

Screenshot of the finished Microsoft Teams message

Azure Function

To begin start with a new C# timer trigger Azure Function that will run once a day.


[Function("MessageTrigger")]
public void Run([TimerTrigger("0 0 * * * *", RunOnStartup = true)] TimerInfo myTimer)
{
    _logger.LogInformation("C# timer function activated.");
}

Inside this function, we will read the template, get the metrics, and post the message to Microsoft Teams.

Reading Azure Metrics

We can make an API call via the C# SDK to get the Azure metrics for a resource. To get the metrics from Azure we will use the Azure.ResourceManager.Compute and Azure.Monitor.Query Nuget packages.

These packages will allow us to get the metrics for a virtual machine resource. Other similar packages are available to get metrics for other resource types.

dotnet add package Azure.ResourceManager.Compute --version 1.3.0
dotnet add package Azure.Monitor.Query --version 1.2.0

These SDKs need to make authenticated calls to Azure. We will use the Azure.Identity package to authenticate using the default credentials on the machine.

dotnet add package Azure.Identity --version 1.10.4

Now we can make the calls needed to get the metrics. For this sample, I am just getting the highest CPU usage for a virtual machine in the last 24 hours. There is a maximum size for a Microsoft Teams message so we will limit the number of results to 10.

private async Task<MetricsModel[]> GetToHighestCPUMinutes(DateTime fromDate, DateTime toDate, string serverId)
{
    var result = new List<MetricsModel>();
    var identity = new DefaultAzureCredential();
    var armClient = new ArmClient(identity);
    var vm = armClient.GetVirtualMachineResource(new ResourceIdentifier(serverId));
    var metricsQueryClient = new MetricsQueryClient(identity);

    //Get the CPU for each minute (average over that minute)
    var queryResults = await metricsQueryClient.QueryResourceAsync(
        vm.Id,
        new[] { "Percentage CPU" }, new MetricsQueryOptions()
        {
            Aggregations = { MetricAggregationType.Average },
            TimeRange = new QueryTimeRange(fromDate, toDate)
        }
    );

    //Extract the values
    foreach (var metric in queryResults.Value.Metrics)
    {
        foreach (var element in metric.TimeSeries)
        {
            foreach (var metricValue in element.Values)
            {
                result.Add(new MetricsModel() { 
                    TimeStamp = metricValue.TimeStamp,
                    Value = metricValue.Average 
                });
            }
        }
    }

    //select the top 10 values
    return result.OrderByDescending(a=>a.Value).Take(10).ToArray();
}

Adaptive Card Template

Adaptive cards allow us to create and post rich content into a Microsoft Teams message. These cards can include forms, images, and action buttons. The website adaptivecards.io has many examples of different cards and includes an interactive designer at adaptivecards.io/designer.

Once you have built your card in the designer you can copy the JSON into the C# code.

You can also use a template where you bind to a data set, as we will do in this example.

Below is the JSON template we will be using, you can paste it into the adaptivecards.io/designer to edit it and see the results.

{
  "type": "AdaptiveCard",
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "version": "1.5",
  "msTeams": {
      "width": "full"
  },
  "body": [
      {
          "type": "TextBlock",
          "text": "${Title}",
          "wrap": true,
          "style": "heading",
          "size": "Large",
          "color": "Attention"
      },
      {
          "type": "TextBlock",
          "text": "${Message}",
          "wrap": true
      },
      {
          "type": "Table",
          "gridStyle": "default",
          "columns": [
              {
                  "width": 3
              },
              {
                  "width": 1
              },
              {
                  "width": 1
              },
              {
                  "width": 1
              },
              {
                  "width": 1
              }
          ],
          "rows": [
              {
                  "type": "TableRow",
                  "cells": [
                      {
                          "type": "TableCell",
                          "items": [
                              {
                                  "type": "TextBlock",
                                  "text": "TimeStamp",
                                  "weight": "Bolder"
                              }
                          ]
                      },
                      {
                          "type": "TableCell",
                          "items": [
                              {
                                  "type": "TextBlock",
                                  "text": "CPU %",
                                  "weight": "Bolder"
                              }
                          ]
                      }         
                  ],
                  "style": "emphasis"
              },
              {
                  "type": "TableRow",
                  "cells": [
                      {
                          "type": "TableCell",
                          "items": [
                              {
                                  "type": "TextBlock",
                                  "text": "${TimeStamp}"
                              }
                          ]
                      },
                      {
                          "type": "TableCell",
                          "items": [
                              {
                                  "type": "TextBlock",
                                  "text": "${string(Value)}"
                              }
                          ]
                      }
                  ],
                  "spacing": "None",
                  "$data": "${Data}"
              }
          ],
          "height": "stretch"
      },
      {
          "type": "ActionSet",
          "actions": [
              {
                  "type": "Action.OpenUrl",
                  "title": "View Portal",
                  "url": "https://portal.azure.com/"
              }
          ]
      }
  ]
}

Reading The Template

As the template is just JSON, we can add it as an embedded resource and then read from the file.

var assembly = Assembly.GetExecutingAssembly();
var resourceName = "AzureMessageStatsToTeams.adaptiveCardTemplate.json";

using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
    throw new FileNotFoundException(resourceName);

using var reader = new StreamReader(stream);
var templateJson = reader.ReadToEnd();

// Create a template instance from the template
var template = new AdaptiveCardTemplate(templateJson);

Now we have loaded the template JSON we can use this to bind to the adaptive card template and produce the final message.

To work with Adaptive Cards in C# we will need to install the following Nuget package:

dotnet add package AdaptiveCards --version 3.1.0

To use two packages the templated version of adaptive cards where we can bind data we will also need an additional package:

dotnet add package AdaptiveCards.Templating --version 1.5.0

With these, we can use the template to bind to the data and produce the final JSON.

var dateFrom = DateTime.UtcNow.AddDays(-1);
var dateTo = DateTime.UtcNow;

var serverData = await GetToHighestCPUMinutes(dateFrom, dateTo, _configuration["serverId"]);

// Create a data model and apply it to the template
var cardJson = template.Expand(new
{
    Title = $"VM Metrics between {dateFrom.ToString("g")} and {dateTo.ToString("g")}",
    Message = "Below are the the top CPU minutes for the last 24 hours.",
    Data = serverData
});

Posting to Teams

There are several ways to post a message to Microsoft Teams. The easiest way is to use the incoming webhook feature. This is a URL that you can post a message to and it will appear in the channel.

In the new version of Microsoft Teams to get the URL you need to go to the channel click on the three dots and then select Manage Channel. From there you can select Edit and add an incoming webhook. Once added you can find the URL to post to.

Screenshot of the finding connectors

Before we post the adaptive card JSON we need to wrap the adaptive card in a message as an attachment.

//Wrap the adaptive card in a teams message
var wrappedTemplate = @"
{
    ""type"": ""message"",
    ""attachments"": [
        {
            ""contentType"": ""application/vnd.microsoft.card.adaptive"",
            ""content"": " + cardJson + @"
        }
    ]
}";

Then we can finally make a simple HTTP post to the webhook URL.

 var payload = new StringContent(wrappedTemplate.Trim(), System.Text.Encoding.UTF8, "application/json");

using var httpClient = new HttpClient();
var response = await httpClient.PostAsync(_configuration["TeamsChannelUri"], payload);

if (!response.IsSuccessStatusCode)
    throw new Exception($"Failed to post to teams: {response.StatusCode}");
else
{
    var responseText = await response.Content.ReadAsStringAsync();
    if (responseText != "1")
    {
        throw new Exception($"Failed to post to teams: {responseText}");
    }
    _logger.LogInformation($"Response from teams: {responseText}");
}

Summary

In this post, we have looked at how we can gather metrics from Azure, create an adaptive card, and post it to a Microsoft Teams channel. This can be a useful way to keep track of metrics and to keep the team informed.

The full code can be found on GitHub.

Title Photo by Austin Distel on Unsplash

Recent and Related Articles