loading...

A real-time Event Grid viewer with serverless SignalR

davidgsola profile image David Sola ・6 min read

A step by step guide to build an Event Grid viewer using serverless SignalR and Azure Functions

In a microservice architecture we might have several decoupled microservices driven by events (aka event-based architecture). At some point, a developer might want to see the current status of the system, just a big picture of what is going on. In a classic monolithic system it might be easy since we only have one big service to look at. In an event-based architecture we can use an event viewer, a simple tool for printing the different events that are occurring in our system in live time.

In this post you will find a step by step guide to build a real-time event viewer for Azure Event Grid based on a serverless SignalR service.

The technology

First, let's take a look to the different services and tools we are going to use in this guide:

  • Azure Event Grid: a service for managing events. It takes care of routing the incoming events to the specific subscription based on filters.

  • Azure Function: a serverless computing service.

  • Azure SignalR: a service for enabling real-time web functionality to applications.

So, how do all these pieces fit together?

  1. SignalR Client application (in this case an Angular based app) must connect to the SignalR service. It makes use of a negotiation API exposed in an Azure Function to get the information to connect to the SignalR service, the SignalR url and a corresponding token.

  2. Our Azure Function will be triggered by new events in the system through Event Grid. Using Functions output binding it will push each event into the serverless SignalR service.

  3. Client application will receive in real-time those events using WebSockets.

Solution

Now let's move on to explaining the whole process step by step in detail.

1. Creating the Azure Function App

To create the Azure Function App we will use the Azure Function Core tools.

npm i -g azure-functions-core-tools@3 --unsafe-perm true

In a fresh new folder we will create the Function App and the Function:

func init
func new -> select http-trigger -> choose a name

Now we have a fresh new Azure Function ready to be launched and tested:

func start
curl http://localhost:7071/api/CloudEventSubscription

2. Subscribing to Event Grid

Azure Event Grid supports two different event schemas:

In this guide, we are going to use Cloud Event Schema v1.0. If you want to use Event Grid Schema you will need to make some minor changes to the validation subscription logic.

Azure Functions have a built-in trigger for Event Grid. However, it only works with Event Grid Schema. Cloud Events subscriptions must be done using a http trigger and doing the subscription validation manually.

To receive events in our Function we need to update the HttpTrigger input binding to allow OPTIONS and POST requests. With CloudEvents schema, OPTIONS verb is used for validating the subscription and POST for receiving the events.

[HttpTrigger(AuthorizationLevel.Function, "options", "post", Route = null)] HttpRequest req

Then add the logic to validate the subscription.

/*
Handle EventGrid subscription validation for CloudEventSchema v1.0
Validation request contains a key in the Webhook-Request-Origin
header, that key must be set in the Webhook-Allowed-Origin response
header to prove to Event Grid that this endpoint is capable of handling CloudEvents events.
*/
if (HttpMethods.IsOptions(req.Method))
{
    if(req.Headers.TryGetValue("Webhook-Request-Origin", out var headerValues))
    {
        var originValue = headerValues.FirstOrDefault();
        if(!string.IsNullOrEmpty(originValue))
        {
            req.HttpContext.Response.Headers.Add("Webhook-Allowed-Origin", originValue);
            return new OkResult();
        }

        return new BadRequestObjectResult("Missing 'Webhook-Request-Origin' header");
    }
}

The Function is ready to be subscribed to Event Grid, but we don't have the Event Grid resource created in Azure yet.

We are going to create the Azure resources directly from the console using Azure CLI. Let's start creating a new resource group.

az group create --name event-grid-viewer-serverless --location northeurope

Now we can create the Event Grid Topic in that resource. To do that we need to make use of an Azure CLI extension.

az extension add -n eventgrid
az eventgrid topic create --resource-group event-grid-viewer-serverless \
   --name topic --location northeurope --input-schema cloudeventschemav1_0

To create the subscription we need to expose the Azure Function using ngrok or any other similar tool.

az eventgrid event-subscription create \ 
   --source-resource-id /subscriptions/<your subscription id>/resourceGroups/event-grid-viewer-serverless/providers/Microsoft.EventGrid/topics/topic \
   --name serverless-signalr-function \
   --endpoint <your public Azure Function url>/api/CloudEventSubscription \ 
   --endpoint-type webhook \
   --event-delivery-schema cloudeventschemav1_0

Then we can test our fresh new subscription sending an event through Event Grid. To make the command easier we can create a sample CloudEvent event in a JSON file.

{
   "specversion":"1.0",
   "type":"com.serverless.event",
   "source":"mysource/",
   "subject":"123",
   "id":"A234-1234-1234",
   "time":"2020-06-14T12:00:00Z",
   "datacontenttype":"application/json",
   "data":"{\"eventProperty\":\"eventValue\"}"
}

The event can be sent using curl but we need to specify the Topic URL and Key that can be retrieved directly from the command line.

// Get the Topic URL
az eventgrid topic show --name topic \
   -g event-grid-viewer-serverless \
   --query "endpoint" --output tsv

// Get the Topic key
az eventgrid topic key list --name topic \
   -g event-grid-viewer-serverless \
   --query "key1" --output tsv

// Send the event
curl --request POST \
   --header "Content-Type: application/cloudevents+json; charset=utf-8" \
   --header "aeg-sas-key: <Topic Key>" \
   --data @event.json <Topic URL>

3. Enabling real-time

As explained at the beginning of the post, we will use SignalR for enabling real-time in our web application. Since we don't have a back-end but a serverless Azure Function for handling the SignalR connection we will use SignalR in serverless mode. We can create it easily with Azure CLI.

az signalr create --name serverless-signalr \
   --resource-group event-grid-viewer-serverless \
   --sku Free_F1 --service-mode Serverless \
   --location northeurope

Then we must install the Azure Functions Binding for SignalR. It will be used to ease the interaction between the Function and SignalR.

func extensions install -p Microsoft.Azure.WebJobs.Extensions.SignalRService -v 1.0.0

Client applications need some credentials to connect to the SignalR service. We need a new negotiate Function that, by making use of the Azure Functions Binding for SignalR, will return the credentials information to connect to SignalR.

[FunctionName("negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
    [SignalRConnectionInfo(HubName = "hubName")] SignalRConnectionInfo connectionInfo)
{
    return connectionInfo;
}

By default, these bindings expect the SignalR ConnectionString to be set in a specific value on the application settings. To enable it locally we must add a new property to the local.settings.json file.

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "AzureSignalRConnectionString": "<Your SignalR Connection String>"
    }
}

Then, we want to push the incoming events from Event Grid to SignalR. First, we need to update the original Function to add an output binding for SignalR.

[SignalR(HubName = "hubName")] IAsyncCollector<SignalRMessage> signalRMessages

And add the logic to push the event to the SignalR service.

if(HttpMethods.IsPost(req.Method))
{
    string @event = await new StreamReader(req.Body).ReadToEndAsync();
    await signalRMessages.AddAsync(new SignalRMessage
    {
        Target = "newEvent",
        Arguments = new[] { @event }
    });
}

4. Client application

The last piece of the puzzle is the client application. First, it will negotiate the credentials with the Function, and then it will wait for new incoming messages from SignalR. In this tutorial we are going to use Angular (and Angular CLI) to create the client application.

ng new viewer-app

There is a npm package @aspnet/signalr that makes the use of SignalR in the client side very easy.

npm i @aspnet/signalr --save

This package handles for you the negotiation step and the connection to SignalR. We can add some simple logic to receive the events from a specific hub and log it into the console.

import * as SignalR from '@aspnet/signalr';

export class AppComponent {

  title = 'viewer-app';

  private hubConnection: SignalR.HubConnection;

  constructor() {
    // Create connection
    this.hubConnection = new SignalR.HubConnectionBuilder()
      .withUrl("http://localhost:7071/api")
      .build();

    // Start connection. This will call the negotiate endpoint
    this.hubConnection
      .start();

    // Handle incoming events for the specific target
    this.hubConnection.on("newEvent", (event) => {
      console.log(event)
    });
  }
}

To wrap up

In this tutorial we have created a simple but effective real-time event viewer for Event Grid using some cool stuff like serverless SignalR. This client application can be easily enhanced to show the events in the page rather than the console.

You can find a full example working with some minor UI enhancements on the following repo: https://github.com/DavidGSola/serverless-eventgrid-viewer

Posted on by:

davidgsola profile

David Sola

@davidgsola

Senior Engineer @ Unit4. Working with .NET, microservices, Azure and serverless stuff.

Discussion

markdown guide