Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Azure Alerts to Slack with F# and azurts!

Azure Alerts to Slack with F#

If you have applications in Azure, there is a good chance you're making use of Azure Monitor with Application Insights or Log Analytics. These can provide useful diagnostic information, healthchecks, and alerting for VM's, serverless function apps, containers, and other services. The alerts can send email and call webhooks, but for the most flexibility, delivering them to an Azure Function allows for robust processing with custom F# code. With the azurts library, this can be used to filter alerts based on content and deliver them to different Slack channels.

Azure to Slack

Composing a Webhook

Azurts uses a "railway oriented programming" technique that is popular in several F# libraries to compose a series of hooks together, where a Hook is a simple function that accepts one type as input and optionally returns Some value or None.

type Hook<'a, 'b> = 'a -> 'b option

A compose operator >=> is included that will bind two Hook functions together, meaning that if the first one returns Some value, then it will pass that value to the second function, which then returns an option as well. Chain as many together as needed to filter or enhance data before the final Hook in the chain, which is typically the one that will send it to Slack. For example

Alert.tryParse >=> Payload.ofAzureAlert "alerts-channel" >=> Payload.sendToSlack webHookConfig

FunctionApp WebHook

By hosting this in an Azure function, Azure Monitor alerts can be delivered to the function, which will convert them to Slack messages and deliver to the channel.

namespace AzureAlertWebhook

open System.IO
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open azurts.Hook
open azurts.AzureAlert
open azurts.SlackWebHook

module WebHook =
  let http = new System.Net.Http.HttpClient ()

  let Run([<HttpTrigger(Methods=[|"POST"|])>] req:HttpRequest) (log:ILogger) =
    async {
      use reader = new StreamReader (req.Body)
      let! alertJson = reader.ReadToEndAsync () |> Async.AwaitTask
      
      let config =
        {
          Client = http
          WebHookUri = System.Uri "https://hooks.slack.com/services/..."
          Token = Some "UakM4mV1GMsDhMvA4KxKEyvnV5d7Q3qmwfScC1PuLmg="
          ErrorCallback = fun (httpStatus, err) -> log.LogError (err)
        }
        
      let hook = Alert.tryParse >=> Payload.ofAzureAlert "alerts-channel" >=> Payload.sendToSlack config
      
      match alertJson |> hook with
      | Some send -> do! send
      | None -> ()
      
      return OkResult ()
    } |> Async.StartAsTask

To actually make use of this, create an Azure FunctionApp for this function and create an HTTP trigger, using IDE, the CLI tools or even building from scratch in F#. Once you publish the FunctionApp, you are provided with a URL to send messages.

Sending Alerts to the FunctionApp WebHook

From Azure Monitor in the Azure Portal, configure Alert Actions:

Manage Actions

Create a new "Action Group" for sending to the FunctionApp, either selecting the FunctionApp directly or by specifying a Webhook where you can provide the URL that is generated when publishing the FunctionApp.

Add Action Group

Now you can create an Alert using a Custom Log Search and choose the Action Group that sends to the FunctionApp. When the Alert is fired, it will send a JSON payload for the alert with records for all of the log entries that triggered that alert in the polling period. So if it polls every 30 minutes and there are 4 items that would trigger that alert in that 30 minutes, it will send a single alert payload that contains those 4 items. The Payload.ofAzureAlert will convert each of them into a Slack message, all with the same Context that shows what log search sent them.

Example Slack Message

Filtering and Broadcasting

The basic workflow of 1. parse, 2. build payload, 3. send to Slack is a nice introduction, but in real world use, richer functionality like filtering and broadcasting are often necessary. For example, alerts may need to be delivered to different channels depending on severity or even custom trace properties. Many included Hook functions can be composed together to define the alert processing logic. The broadcast function accepts a list of other Hook functions and will iterate through each of them, executing each Hook that doesn't return a None value for the option. Combined with Hook functions from the Filters module, alerts can be routed to one or more channels based on content.

For example, this hook sends high severity alerts to the critical-alerts-channel, sends lower severity alerts to regular-alerts-channel and also sends all alerts for ResourceOne to the resource-one-alerts channel.

Alert.tryParse >=> broadcast
  [
    Filters.minimumSeverity 3 >=> Payload.ofAzureAlert "critical-alerts-channel" >=> sendToSlack config
    Filters.maximumSeverity 2 >=> Payload.ofAzureAlert "regular-alerts-channel" >=> sendToSlack config
    Filters.fieldValue "customDimensions_ResourceName" "ResourceOne" >=> Payload.ofAzureAlert "resource-one-alerts" >=> sendToSlack config
  ]

Just the Slack WebHook Please

It's useful to build Slack messages outside of Azure Monitor alerting, so it's worth noting that the Slack WebHook DSL is usable on its own to be composed with other solutions.

A Slack message payload is just the channel and a list of Blocks:

type Payload =
  {
    Channel : string
    Blocks : Block list
  }

The Blocks used to build messages are implemented by the Block discriminated union:

type Block =
  | Context of Context
  | Divider of Divider
  | Section of Section

The message itself is made of a lot of text, which can be either simple markdown or some labeled text elements which are rendered to show a labeled field that Slack will layout in one or two columns.

type Text =
  | Markdown of string
  | LabeledText of Label:string * Content:string

The divider is pretty simple and a single case just for dividing messages:

type Divider =
  | Divider`

The context can provide a simple bit of subtext in a message, built from a list of Text elements:

type Context =
    | Elements of Text list

A section can contain either a single Text element or Fields which is a list of Text elements.

type Section =
  | Text of Text
  | Fields of Text list

There is a function included in the SlackPayload module to create a payload from an Azure Monitor Alert that can be used as a guide to implement your own message payloads.

Composing with other API's

It's worth mentioning that this is useful for webhooks beyond just Azure Monitor alerts because composition makes it possible to bring your own records and objects, and format them into a Slack message. Or instead of delivering to Slack, the could deliver to other services, like certain critical messages could deliver to a PagerDuty hook. Anything that can be described as type Hook<'a, 'b> = 'a -> 'b option can be integrated into the alert processing pipeline.

What did I learn?

Building the Slack webhook DSL was fun but also sometimes frustrating. The responses returned from the API aren't very useful when there is a payload that isn't structured properly. There are also some lightly documented rules like how a text field has a maximum length of 3000 characters. The API reference is very thorough, and the SlackWebHook.fs module implements it to ensure messages meet Slack's API requirements.

I also learned that the built in webhook payload formatting built into Azure Monitor is relatively limited. I initially asked myself Why not use a Slack webhook directly - why put F# in the middle? While that can work, the resulting Slack message leaves a lot to be desired. Basic formatting of alert information is possible, but little else, meaning anyone watching the Slack channel will have to go back to Azure Monitor to get the details.

Since we moved from using the basic webhook to using the formatted messages through azurts, it's been a lot easier for people to see and act on alerts, and as a result, people resolve and take steps to prevent them. The expressiveness of F# and "railway composition" makes it easy for people to understand how alerts are delivered, and the type safety enables others to contribute without fear of breaking the alerts that are in place. Please use it as you like and contributions are welcome to extend the alert processing and message formatting capabilities!


Logo's are used per the guidelines from Azure and Slack and originals can be found on the Azure and Slack websites.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.