Azure Functions Monitor Pattern Example

Azure Functions Monitor Pattern Example
The Azure Functions Monitor Pattern helps to easily check over or poll long running processes that have a flexible finishing time

Azure Functions are incredible at being able to complete computations on our behalf. The Monitor Pattern specifically allows an Azure Function to consistently check for the status of a long running process that has a completion time that is irregular or unfixed. With the Monitor pattern, we set a large time window for the task to complete within, then we also instruct the Function to keep checking on the status of the task over a known interval period (or even a flexible interval period should we choose depending on code logic). We also don't have to write separate polling code.

The following shows a short and very simplified example of an HTTP triggered Durable Function using a Monitor Pattern, that will be checking on a web page over a set period of 2months to see whether the Artemis-I Space mission has launched yet, checking on a daily interval, and then alert a recipient by email if it has launched:

using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Text;
using Microsoft.AspNetCore.Mvc;  //also see Project Packages section

//your namespace encapsulation here
   //your class encapsulation here

[FunctionName("MonitorPage_Trigger")]
public static async Task<IActionResult> HttpStart(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
ILogger log, [DurableClient] IDurableOrchestrationClient starter)
{
    string instanceId = await starter.StartNewAsync("BeginMonitor", null);

    log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

    return new OkObjectResult($"The Page monitor has started, you will get an email alert ONLY when Artemis-I has launched, if not, assume it hasn't launched");
}

The Http Trigger Function (Client Function) that will start an Orchestration

The Orchestrator code:

private static readonly HttpClient httpClient = new HttpClient();

[FunctionName("BeginMonitor")]
public static async Task BeginMonitor(
[OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
{

    if (!context.IsReplaying) {
    log.LogInformation($"Artemis launch page monitoring has begun"); 
    }

    //this is your cut off period where the Function will fully stop
    DateTime cutOffPeriod = context.CurrentUtcDateTime.AddMonths(2);

    string launchStatus = string.Empty;
    while(context.CurrentUtcDateTime < cutOffPeriod)
    {
        launchStatus =
          await context.CallActivityAsync<string>("CheckLiftOff", "has it launched yet!!");
        if (launchStatus == "We are going to space!!")
        {
            //send an alert here and then self-shutdown
            //also refer to Logic App section further down
            string logicAppUrl = "[YOUR_LOGIC_APP_URL]";

            var uri = new Uri(logicAppUrl);
            var request = new HttpRequestMessage(HttpMethod.Post, uri)
            {
                Content = new StringContent("{" + 
                $"\"launchStatus\":\"{launchStatus}\" " + "}",
                Encoding.UTF8, "application/json")
            };

            //call the logic app to send an email
            await httpClient.SendAsync(request);
            log.LogInformation($"Artemis launch page monitoring is now ending");
            break;
        }
        else
        {
            //try again on the next interval (in 24 hrs)
            log.LogInformation($"Artemis launch page monitoring is scheduling next retry");

            var nextCheckpoint = context.CurrentUtcDateTime.AddDays(1);

            await context.CreateTimer(nextCheckpoint, CancellationToken.None);
        }
    }

}

Orchestration Function to keep checking the liftOff status. If not, then check again in 1 day, until 2months have passed.

The Activity Function fetches a web page and checks for satisfaction of conditions:

[FunctionName("CheckLiftOff")]
public static async Task<string> CheckLiftOff([ActivityTrigger] string inputData,
ILogger log)
{
    log.LogInformation($"Checking liftoff!!");
    var page = await httpClient.GetAsync("https://www.nasa.gov/artemis-1");
    var pageText = await page.Content.ReadAsStringAsync();

    //heavily simplified assertions here!!

    List<string> keyData = new List<string>()
    {
        "Artemis-I has launched",
        "Artemis-I has been launched",
        "Artemis-I MECO", //main engine cutoff
        "Artemis-I liftoff",
        "Godspeed",
        "MAX-Q reached",
        "Mach 1 reached",
        "Go Artemis go"
        //"Bravo" , "Capcom" // useful indicators of flight takeoff but unlikely 
        //to be written in a webpage,largely used in radio dialogue
    };

    var liftOffIndicatorCount =
      keyData.Count(x => pageText.ToLower().Contains(x.ToLower()));

    if (liftOffIndicatorCount > 1)
    {
    	log.LogInformation($"liftoff Confirmed!");

    	return "We are going to space!!";
    }
    else
    {
    	return "Not yet launched :(";
    }

}
    

A simple check to see if the web page has been updated with certain information we know will indicate a successful launch

Project packages

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.8.1" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

In Azure, the Function app is created on a Consumption plan as we only ever need to call the Http Function once. The Monitor Pattern we set up will manage the lifecycle of the process for us.

Logic App Setup for alerts

For alerting we can use any kind of alert we choose when our conditions are met. Here, I chose a Logic App on a Consumption Plan that will send an alert to a known email address:

Setup for the Logic App that is triggered by an HTTP POST Request

The email option chosen here was Gmail Send Email V2. You will need grant the Azure Logic App permissions to a Signed-in Gmail account as it will use this Gmail account as the 'From' address:

The email to be sent to a recipient when the launch is confirmed automatically from the Function

Conclusions

Here are some reasons why we may want to use the Monitor Pattern in this particular case:

  • The launch of the Artemis mission is not entirely fixed. We know it's scheduled 'some time' in mid-November at the time of this writing but we don't know the exact time. Therefore we want to set our Azure Function interval check to every single day (24hrs or 1day) over a period that covers all of November because space launches can get delayed or done earlier in response to numerous variables.
  • We will only call the Function App once, the rest is self managed by our Montior pattern, meaning that based on our interval, and other code logic, the launch is more or less guaranteed to be captured and we will be alerted about it when code logic is satisfied.
  • Unlike a Timer-triggered Function that will forever execute based on the CRON schedule, this HTTP Triggered Durable Function will manage it's own lifecycle and not execute further until triggered again, all without needing to write extra polling code or callback Urls.
  • Ultimately, we probably want to use more nuanced and substantial ways of checking the contents of the web page compared to using phrase checks.

Cover Image Credit: Brian McGowan / Unsplash