DEV Community

Cover image for Serverless Event Driven Speeding Infraction Management System using Azure Event Grid, Functions and Cognitive Services
Mandar Dharmadhikari
Mandar Dharmadhikari

Posted on

Serverless Event Driven Speeding Infraction Management System using Azure Event Grid, Functions and Cognitive Services

I have always been a fan of movies. When I think of fast cars, comedy, and swag, Rowan Atkinson's Johnny English driving a Rolls Royce or Aston Martin always comes to my mind. He always ends up speeding his vehicle and gets captured in the speed detecting camera and believe me his facial expressions are always hilarious. Have a look!
Johnny English Captured on Camera

Though Johnny English destroys the camera with his awesome car fired missile, this gives us a real life example where we can use the power of serverless computing to process the captured images of the driving vehicles.

In this article we will discuss how we can implement a event driven system to process the images uploaded by camera to cloud environment. We will apply Azure flavor to it.

Problem Statement

The people of Abracadabra country are speed aficionados and some times in their zeal of enjoying the thrill of fast speed, they cross the legal speed limit laid down by the local government. Hence the Minister of Transport has decided to install speed triggered cameras at major intersections in all the major cities pan Abracadabra. The cameras will capture the images of the vehicles and upload the images to the cloud. The process is

  1. The cameras upload captured images to the cloud storage as blobs along with all the details like the district where infraction occurred etc.
  2. The in cloud system detects the registration number and notifies the registered owner of the speeding infractions.

Caveat In past due to presence of faces in the government captured photos, the citizens had sued the government over privacy infringement. So the minister wants all the faces in the captured images blurred out.

Azure Based Solution

Let us have a look at how we can implement the process laid out by the Transport Minister of Abracadabra government.

As is the case with software engineering, we can implement a system in multiple ways. Today we will implement the requirement using event driven architecture principles. Various components in the system will react to various events and perform their intended tasks and if required emit events of their own. To know more about the event driven architecture, read Martin Fowlers bliki article
What do you mean by โ€œEvent-Drivenโ€?.

I digress, let us get back to the task at hand.

Let us Break down each requirement and have a look at what is available to us.

Task Azure Tech Available
Upload Image to Cloud Azure Blob Storage container
Extract Registration Number Azure Cognitive Service: Computer Vision : OCR
Maintain a Registration Database Azure CosmosDb collection
Maintain Created tickets against offenders Azure CosmosDb Collection
Send Notification Emails Send Grid(Not Azure)
Blur Faces Azure Cognitive Service: Face API
Code blocks to execute gelling logic for all components Azure Functions
Create event sinks for code to tap onto Azure Event Grid Custom and System topic
Log End to End Transactions Application Insights

Based on the components, following architecture can be visualized.

Serverless Speeding Infraction Management System Architecture

The logical flow of the control in the system can be represented as below.

Logical Flow of the system

Based on the architecture the following is a list of the events consumed or emitted by components of the system.

Event Type When
Blob Created System Event When a blob is created in a container in storage account.
NumberExtractionCompleted Custom Event When the registration number of the vehicle in image is extracted out successfully.
SpeedingTicketCreated Custom Event When a record is successfully created inside the SpeedingInfractions collection.
Exceptioned Custom Event Whenever the system experiences an irrecoverable exception.

Understanding The Architecture

Let us look in depth at each component and why it is present in our architecture.

Azure Cognitive Service: Computer Vision

Azure Computer vision is a Cognitive service offered my Microsoft which caters to Image analysis. It is trained and managed by Microsoft using some predefined algorithms
Some Salient features of Computer Vision service are

  1. Analyze images using pre-defined and trained models and extract rich information from the image.
  2. Perform OCR which enables us to extract printed and handwritten text from the images with a very high accuracy
  3. Recognize famous personalities, places landmarks etc.
  4. Generate thumbnails

Computer vision supports REST based API calls or use of SDKs for development using .NET. The service can process image which can be passed to the underlying API as a byte array or using URL to the image. Billing model for this is Pay per Usage. The Free tier offerings are very generous which can be easily used for experimentation.

In Our Case: Computer Vision API will be used to extract out the alpha numeric registration number.

Azure Cognitive Service: Face API

Azure Face API is a Cognitive service offered my Microsoft which caters to Face analysis. It is trained and managed by Microsoft using some predefined algorithms. Some salient features of Face API are

  1. Detect faces
  2. Recognize faces etc.

Face API supports REST based API calls or use of SDKs for development using .NET. The service can process image which can be passed to the underlying API as a byte array or using URL to the image. Billing model for this is Pay per Usage. The Free tier offerings are very generous which can be easily used for experimentation.

In Our Case: Face API will be used to detect the presence of faces in the captured images.

There are two ways to create Cognitive services in Azure

  1. Create a bundle of services called the cognitive services and it will deploy all the services and we can use any service with same subscription key.
  2. Create individual subscriptions for each Cognitive Service.

I generally prefer the later option as it allows me granular control over the keys in case the key is compromised. But for beginners refer to Quickstart: Create a Cognitive Services resource using the Azure portal .This lists both ways that we just discussed above.

Azure Cosmos DB

Azure Cosmos DB is a fully managed NoSQL database suitable for modern day applications. It provides single digit millisecond response times and is highly scalable. It supports multiple data APIs like SQL, Cassandra, MongoDB API, Gremlin API, Table API etc. Since Cosmos DB is fully managed by Azure we do not have to worry about database administration, updates patching etc. It also provides capacity management as it provides a serverless model or automatic scaling. To try Azure Cosomos DB for free please refer Try Azure Cosmos DB for free.

In Our Case:

  1. We will store the record of the registered vehicle owner in a collection. This collection will be partitioned based on the district under which the vehicle is registered.
  2. We will store the created ticket in a collection. This collection will be partitioned based on the district under which the infraction occurred. Setting such partition keys will ensure that we have a well partitioned collection and no one partition will behave as a hot partition.

Azure Event Grid

Azure Event Grid is a fully managed event handling service provided by Azure. It allows us to create systems which work on principles of event driven architecture. It is tightly baked in Azure and hence a lot of activities that are done on Azure will produce events onto which we can tap. E.g. When a blob is created, a event notification is generated an put onto a system topic in event grid. We can react to this event using Logic Apps or Azure Functions very easily. (This is what we will do by the way ๐Ÿ˜‰). We can also create custom topics and push custom events native to our application. (Another thing we will do). To learn more about how to work with Azure Event Grid, please refer What is Azure Event Grid?

In Our Case:

  1. We will tap into the "Blob Created" event emitted when the images are uploaded to blob container. This event will kick start the flow.
  2. Other systems will subscribe or publish custom events e.g. "NumberExtractionCompleted" . Each system will publish a custom event which will contain the state change in case there were some side effects in the system. (A precious thing called Event Carried State Transfer).

Azure Functions

Azure functions provide a code based way of creating small pieces of codes that perform task or list of tasks. Azure Functions are useful when we want to write stateless idempotent functions. In case complex orchestrations are required, we can use its cousin Durable Functions. Durable functions work with the durable task framework and provide a way to implement long run stateful orchestrations. To learn more about Azure Functions refer Introduction to Azure Functions. To learn about Durable Functions What are Durable Functions? is a good starting point
In Our Case: We will use Azure functions to create pieces of workflow which react to system events and custom events and work together to provide the desired output from the system. we can use Durable Functions in our case as well, but to reduce the complexity of the code, I have decided to stick with Azure Functions.

Azure Blob Storage

Azure Blob Storage is the Azure solution for storing massive amount of unstructured data in the cloud. Blob Storage is useful when we want to stream images, videos, static files, store log files etc. We can access the blob storage through language specific SDKs or through the platform agnostic REST APIS. To learn more about the blob storage, What is Azure Blob storage? is a good starting point.
In Our Case:

  1. The system managing the camera and uploads to the cloud will upload the images to the sourceimages container.
  2. The Serverless system will detect faces and upload the images with blurred faces to the blurredimages container.
  3. If the system encounters an irrecoverable error, the particular uploaded image will be move to exceptions folder for some one to manually process it.

SendGrid

SendGrid will be used to send out notifications to the registered users. SendGrid is a third party service. To set up a send grid account for free plan register at Get Started with SendGrid for Free

Building the Solution

As you can understand most of the components we have used in the architecture are already deployed and available for use to consume. So let us know look at how the azure functions are built.

Note: I am deliberately going to avoid adding the actual logic in the article as there are over thousand lines of code and the article will grow out of proportions and loose the sight of understanding the main topic. I highly urge to consult the GitHub repository in order to understand the code written

Prerequisites

In order to build a Azure Function Solution, we need following tools and nuget packages.

Tools and Frameworks

  1. .NET Core 3.1 as the Azure Functions project targets this framework ( Download .NET Core 3.1 )
  2. Azure Functions core tools (Work with Azure Functions Core Tools)
  3. Visual Studio Code or Visual Studio 2019 or any IDE or Editor capable of running .NET core applications.

Optional:

  1. Azure Storage Explorer(Download)
  2. Azure CosmosDB Emulator (Work locally with Azure CosmosDB)

NuGet Packages

Following screenshot shows all the nuget packages required in solution.

Nuget Packages and Other settings

Solution Structure

Once set up the solution looks like following.
Alt Text

Understanding the Solution

Full Disclosure: Azure functions has plethora of bindings which allows us to import or push data out to many resources like CosmosDB, Event Grid etc. It is my personal preference to write Interfaces to establish communications with external entities. So in this solution, I have implemented interfaces for each external system to which the solution communicates.

Interfaces

CosmosDB

In order to create speeding ticket or querying data from the ticketing collection or vehicle registration collections, I have created a IDmvDbHandler interface its contract is shown below.

public interface IDmvDbHandler
    {
        /// <summary>
        /// Get the information of the registered owner for the vehicle using the vehicle registration number
        /// </summary>
        /// <param name="vehicleRegistrationNumber">Vehicle registration number</param>
        /// <returns>Owner of the registered vehicle</returns>
        public Task<VehicleOwnerInfo> GetOwnerInformationAsync(string vehicleRegistrationNumber);

        /// <summary>
        /// Create a speeding infraction ticket against a vehicle registration number
        /// </summary>
        /// <param name="ticketNumber">Ticket number</param>
        /// <param name="vehicleRegistrationNumber">Vehicle registration number</param>
        /// <param name="district">The district where the infraction occured</param>
        /// <param name="date">Date of infraction</param>
        /// <returns></returns>
        public Task CreateSpeedingTicketAsync(string ticketNumber, string vehicleRegistrationNumber, string district, string date);

        /// <summary>
        /// Get the ticket details
        /// </summary>
        /// <param name="ticketNumber">Tikcet number</param>
        /// <returns>Speeding Ticket details</returns>
        public Task<SpeedingTicket> GetSpeedingTicketInfoAsync(string ticketNumber);


    }

Enter fullscreen mode Exit fullscreen mode

This interface is implemented using the CosmosDB SDK. It is implemented in the class CosmosDmvDbHandler

Face API

To communicate with the Face API, following IFaceHandler interface is created.

/// <summary>
        /// Detect all the faces in the image specifed using an url
        /// </summary>
        /// <param name="url">Url of the image</param>
        /// <returns>List of all the detected faces</returns>
        public Task<IEnumerable<DetectedFace>> DetectFacesWithUrlAsync(string url);

        /// <summary>
        /// Detect all the faces in an image specified using a stream
        /// </summary>
        /// <param name="imageStream">Stream containing the image</param>
        /// <returns>List of all the detected faces</returns>
        public Task<IEnumerable<DetectedFace>> DetectFacesWithStreamAsync(Stream imageStream);

        /// <summary>
        /// Blur faces defined by the detected faces list
        /// </summary>
        /// <param name="imageBytes">The byte array containing the image</param>
        /// <param name="detectedFaces">List of the detected faces in the image</param>
        /// <returns>Processed stream containing image with blurred faces</returns>
        public Task<byte[]> BlurFacesAsync(byte[] imageBytes, List<DetectedFace> detectedFaces);
Enter fullscreen mode Exit fullscreen mode

This interface is implemented with the help of Face API SDK available on NuGet. It is implemented in class FaceHandler class.

ComputerVision

To extract out the vehicle registration number from the image using Optical Character Recognition, the interface IComputerVisionHandler is created.

    public interface IComputerVisionHandler
    {
        /// <summary>
        /// Extract registration number from image specified using its url
        /// </summary>
        /// <param name="imageUrl">Url of the image</param>
        /// <returns>Extracted registration number</returns>
        public Task<string> ExtractRegistrationNumberWithUrlAsync(string imageUrl);

        /// <summary>
        /// Extract registration number from image specified using the stream
        /// </summary>
        /// <param name="imageStream">Stream containing the image</param>
        /// <returns>Extracted  registration number</returns>
        public Task<string> ExtractRegistrationNumberWithStreamAsync(Stream imageStream);

    }
Enter fullscreen mode Exit fullscreen mode

This interface is implemented with the help of the Computer Vision SDK. It is implemented in ComputerVisionHandler class.

Sending Notification

In order to send out notifications to the users, the interface IOwnerNotificationHandler is used.

    public interface IOwnerNotificationHandler
    {
        /// <summary>
        /// Notify the Owner of the Vehicle
        /// </summary>
        /// <param name="ownerNotificationMessage">Information of the vehicle Owner</param>
        /// <returns></returns>
        public Task NotifyOwnerAsync(OwnerNotificationMessage ownerNotificationMessage);
    }

Enter fullscreen mode Exit fullscreen mode

This interface is implemented using the SendGrid SDK. It is implemented in SendGridOwnerNotificationHandler class.

Publishing Events

The IEventHandler interface describe the methods used while publishing messages to the event sink.

    public interface IEventHandler
    {
        /// <summary>
        /// Publish a custom event to the event sink
        /// </summary>
        /// <param name="customEventData">Customn Event Data</param>
        /// <returns></returns>
        public Task PublishEventToTopicAsync(CustomEventData customEventData);
    }
Enter fullscreen mode Exit fullscreen mode

This interface is implemented using the Azure Event Grid SDK in the EventGridHandler class.

The azure functions in solution will emit events which conform to the event grid schema. In order to emit custom solution data, instances of the CustomEventData class are serialized to the data node. The custom data published by the functions follows below contract

public class CustomEventData
    {

        [JsonProperty(PropertyName = "ticketNumber")]
        public string TicketNumber { get; set; }

        [JsonProperty(PropertyName = "imageUrl")]
        public string ImageUrl { get; set; }

        [JsonProperty(PropertyName = "customEvent")]
        public string CustomEvent { get; set; }

        [JsonProperty(PropertyName = "vehicleRegistrationNumber")]
        public string VehicleRegistrationNumber { get; set; }

        [JsonProperty(PropertyName = "districtOfInfraction")]
        public string DistrictOfInfraction { get; set; }

        [JsonProperty(PropertyName = "dateOfInfraction")]
        public string DateOfInfraction { get; set; }


    }
Enter fullscreen mode Exit fullscreen mode

A sample event emitted when the number extraction is completed is as following

{
  "id": "3c848fdc-7ad6-47e9-820d-b9f346ba7f7a",
  "subject": "speeding.infraction.management.customevent",
  "data": {
    "ticketNumber": "Test",
    "imageUrl": "https://{storageaccount}.blob.core.windows.net/sourceimages/Test.png",
    "customEvent": "NumberExtractionCompleted",
    "vehicleRegistrationNumber": "ABC6353",
    "districtOfInfraction": "wardha",
    "dateOfInfraction": "14-03-2021"
  },
  "eventType": "NumberExtractionCompleted",
  "dataVersion": "1.0",
  "metadataVersion": "1",
  "eventTime": "2021-03-14T14:51:41.2448544Z",
  "topic": "/subscriptions/{subscriptionid}/resourceGroups/rg-dev-stories-dotnet-demo-dev-01/providers/Microsoft.EventGrid/topics/ais-event-grid-custom-topic-dev-01"
}
Enter fullscreen mode Exit fullscreen mode

Blob Management

In order to access the blobs from the solution, interface IBlobHandler interface is used to describe the contracts.

 public interface IBlobHandler
    {
        /// <summary>
        /// Retrieve the metadata associated with a blob
        /// </summary>
        /// <param name="blobUrl">Url of the blob</param>
        /// <returns>Key value pairs of the metadata</returns>
        public Task<IDictionary<string, string>> GetBlobMetadataAsync(string blobUrl);

        /// <summary>
        /// Upload the stream as a blob to a container
        /// </summary>
        /// <param name="containerName">Name of the container where the stream is to be uploaded</param>
        /// <param name="stream">Actual Stream representing object to be uploaded</param>
        /// <param name="contentType">Content type of the object</param>
        /// <param name="blobName">Name with which the blob is to be created</param>
        /// <returns></returns>
        public Task UploadStreamAsBlobAsync(string containerName, Stream stream, string contentType,
            string blobName);

        /// <summary>
        /// Download Blob contents and its metadata using blob url
        /// </summary>
        /// <param name="blobUrl">Url of the blob</param>
        /// <returns>Blob information</returns>
        public Task<byte[]> DownloadBlobAsync(string blobUrl);

        /// <summary>
        /// Copy the blob from one container to another in same storage account using the url of the source blob
        /// </summary>
        /// <param name="sourceBlobUrl">Url of the source blob</param>
        /// <param name="targetContainerName">Destination container name</param>
        /// <returns></returns>
        public Task CopyBlobAcrossContainerWithUrlsAsync(string sourceBlobUrl, string targetContainerName);


    }
Enter fullscreen mode Exit fullscreen mode

This interface is implemented in the solution using Azure Blob Storage SDKs in AzureBlobHandler class.

Options

Azure functions support the Options pattern to access a group of related configuration items. (Working with options and settings in Azure functions)

Following a sample options class to access the details of the email details from the application settings of the Azure Function App.

public class SendGridOptions
    {
        public string EmailSubject { get; set; }

        public string EmailFromAddress { get; set; }

        public string EmailBodyTemplate { get; set; }
    }
Enter fullscreen mode Exit fullscreen mode

Below is an example of local.settings.json which contains SendGridOptions

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "BlobOptions:BlurredImageContainerName":"",
        "BlobOptions:CompensationContainerName":"",
        "Bloboptions:UploadContentType":"",
        "BlobStorageConnectionKey":"",
        "ComputerVisionEndpoint":"",
        "ComputerVisionSubscriptionKey":"",
        "CosmosDbOptions:DatabseId":"",
        "CosmosDbOptions:InfractionsCollection":"",
        "CosmosDbOptions:OwnersCollection":"",
        "DmvDbAuthKey":"",
        "DmvDbUri":"",
        "EventGridOptions:TopicHostName":"",
        "EventGridTopicSasKey":"",
        "FaceApiEndpoint":"",
        "FaceApiSubscriptionKey":"",
        "SendGridApiKey":"",
        "SendGridOptions:EmailBodyTemplate":"",
        "SendGridOptions:EmailFromAddress":"",
        "SendGridOptions:EmailSubject":""
    }
}
Enter fullscreen mode Exit fullscreen mode

Registering Dependencies

Azure Functions support dependency injection to make our code more testable and loosely coupled. As with .NET core based web apps, the dependency injection in Azure Functions is implemented in the Startup class of the project. Refer Dependency Injection in Azure Functions
Following code snippet shows the skeleton of the startup class and the Configure method where we will register all of our dependencies.

[assembly: FunctionsStartup(typeof(Speeding.Infraction.Management.AF01.Startup))]
namespace Speeding.Infraction.Management.AF01
{
    public class Startup : FunctionsStartup
    {

        public override void Configure(IFunctionsHostBuilder builder)
        {


        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Following code snippet shows how we configure options in the startup classes. Again I am using the example of the SendGridOptions.


 builder.Services.AddOptions<SendGridOptions>()
                .Configure<IConfiguration>((settings, configuration) =>
                {
                    configuration.GetSection(nameof(SendGridOptions)).Bind(settings);
                });

Enter fullscreen mode Exit fullscreen mode

All the major SDK clients are registered as singletons so that the same instance of the client is used through out the lifetime of the application. The code snippet below shows how to do this.

builder.Services.AddSingleton<IDocumentClient>(
                    x => new DocumentClient(
                            new Uri(
                                    Environment.GetEnvironmentVariable("DmvDbUri")
                                ),
                            Environment.GetEnvironmentVariable("DmvDbAuthKey")
                        )
                );

            builder.Services.AddSingleton<IComputerVisionClient>(
                    x => new ComputerVisionClient(
                            new Microsoft.Azure.CognitiveServices.Vision.ComputerVision.ApiKeyServiceClientCredentials(
                                    Environment.GetEnvironmentVariable("ComputerVisionSubscriptionKey")
                                )

                        )
                    {
                        Endpoint = Environment.GetEnvironmentVariable("ComputerVisionEndpoint")
                    }
                );

            builder.Services.AddSingleton<IFaceClient>(
                    x => new FaceClient(
                            new Microsoft.Azure.CognitiveServices.Vision.Face.ApiKeyServiceClientCredentials(
                                    Environment.GetEnvironmentVariable("FaceApiSubscriptionKey")
                                )
                        )
                    {
                        Endpoint = Environment.GetEnvironmentVariable("FaceApiEndpoint")
                    }
                );

            builder.Services.AddSingleton<BlobServiceClient>(
                    new BlobServiceClient(
                            Environment.GetEnvironmentVariable("BlobStorageConnectionKey")
                        )
                );
            builder.Services.AddSingleton<IEventGridClient>(
                    new EventGridClient(
                            new TopicCredentials(
                                Environment.GetEnvironmentVariable("EventGridTopicSasKey")
                            )
                        )
                );

            builder.Services.AddSingleton<ISendGridClient>(
                    new SendGridClient(
                            Environment.GetEnvironmentVariable("SendGridApiKey")
                        )
                );

Enter fullscreen mode Exit fullscreen mode

Other interfaces and their implementations are registered as following.

 builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
            builder.Services.AddTransient<IBlobHandler, AzureBlobHandler>();
            builder.Services.AddTransient<IDmvDbHandler, CosmosDmvDbHandler>();
            builder.Services.AddTransient<IFaceHandler, FaceHandler>();
            builder.Services.AddTransient<IComputerVisionHandler, ComputerVisionHandler>();
            builder.Services.AddTransient<IEventHandler, EventGridHandler>();
            builder.Services.AddTransient<IOwnerNotificationHandler, SendGridOwnerNotificationHandler>();
Enter fullscreen mode Exit fullscreen mode

Azure Functions

Following is the list of all the functions created as part of the solution. As the concept of dependency injection is used, we can safely create non static classes and inject the necessary dependencies in the constructor of the class containing the Azure Function(s).

ExtractRegistrationNumber

This azure function is triggered when a blob is uploaded to the sourceimages container. Once the function completes it either emits a NumberExtractionCompleted event if successful or Exceptioned event if exception occurs. The skeleton for the function is shown below.

public class NumberPlateController
    {
        private readonly IComputerVisionHandler _computerVisionHandler;
        private readonly IEventHandler _eventHandler;
        private readonly IBlobHandler _blobHandler;

        public NumberPlateController(IComputerVisionHandler computerVisionHandler,
            IEventHandler eventHandler,
            IBlobHandler blobHandler)
        {
            _computerVisionHandler = computerVisionHandler ??
                throw new ArgumentNullException(nameof(computerVisionHandler));

            _eventHandler = eventHandler ??
                throw new ArgumentNullException(nameof(eventHandler));

            _blobHandler = blobHandler ??
                throw new ArgumentNullException(nameof(blobHandler));
        }


        [FunctionName("ExtractRegistrationNumber")]
        public async Task ExtractRegistrationNumber(
                [EventGridTrigger] EventGridEvent eventGridEvent,
                ILogger logger
            )
        {


        }

    }

Enter fullscreen mode Exit fullscreen mode

CreateSpeedingTicket

This function is triggered when a NumberExtractionCompleted event occurs. This function creates the speeding ticket in the SpeedingInfractions collection in the CosmosDB. Following is a sample ticket created by the function.

{
    "id": "4f81c43a-53a8-42c0-a61a-10a40680f836",
    "ticketNumber": "2f4e63a3-b9c5-4fe2-ab5d-745920b905f2",
    "vehicleRegistrationNumber": "MLK 6353",
    "district": "nagpur",
    "date": "15-03-2021",
    "_rid": "oaMUAJlZ1PwMAAAAAAAAAA==",
    "_self": "dbs/oaMUAA==/colls/oaMUAJlZ1Pw=/docs/oaMUAJlZ1PwMAAAAAAAAAA==/",
    "_etag": "\"13008756-0000-2000-0000-604f39e60000\"",
    "_attachments": "attachments/",
    "_ts": 1615804902
}
Enter fullscreen mode Exit fullscreen mode

The function emits SpeedingTicketCreated event if successful and Exceptioned if exception occurs.
The skeleton for the function looks as follows.

 public class TicketController
    {
        private readonly IDmvDbHandler _dmvDbHandler;
        private readonly IEventHandler _eventHandler;

        public TicketController(IDmvDbHandler dmvDbHandler, 
            IBlobHandler blobHandler,
            IEventHandler eventHandler)
        {
            _dmvDbHandler = dmvDbHandler ??
                throw new ArgumentNullException(nameof(dmvDbHandler));

            _eventHandler = eventHandler ??
                throw new ArgumentNullException(nameof(eventHandler));
        }

        [FunctionName("CreateSpeedingTicket")]
        public async Task CreateTicket(
                [EventGridTrigger] EventGridEvent eventGridEvent,
                ILogger logger
            )
        {

        }

    }
Enter fullscreen mode Exit fullscreen mode

DetectAndBlurFaces

This function is triggered when SpeedingTicketCreated event occurs. This function uses the Face API and detects the presence of faces in the image by passing the URL of the image blob. If a face is detected, the function then blurs the face using the ImageProcessorCore SDK and then uploads the blurred image as a blob to blurredimages container. This function emits Exceptioned event if exception occurs.

Following is the skeleton for the function.

public class FaceController
    {
        private readonly IFaceHandler _faceHandler;
        private readonly IBlobHandler _blobHandler;
        private readonly IEventHandler _eventhandler;
        private readonly BlobOptions _options;

        public FaceController(IFaceHandler faceHandler,
            IBlobHandler blobHandler,
            IEventHandler eventHandler,
            IOptions<BlobOptions> settings)
        {
            _faceHandler = faceHandler ??
                throw new ArgumentNullException(nameof(faceHandler));

            _blobHandler = blobHandler ??
                throw new ArgumentNullException(nameof(blobHandler));

            _eventhandler = eventHandler ??
                throw new ArgumentNullException(nameof(eventHandler));

            _options = settings.Value;

        }

        [FunctionName("DetectAndBlurFaces")]
        public async Task DetectAndBlurFaces(
            [EventGridTrigger] EventGridEvent eventGridEvent,
                ILogger logger
            )
        {

        }
    }

Enter fullscreen mode Exit fullscreen mode

NotifyRegisteredOwner

This function is triggered by BlobCreated event which occurs when a blob is uploaded to the blurredimages container. This function queries speeding ticket data from the SpeedingInfractions collection, collects registered owner details from RegisteredOwners collection and then sends out an email using the SendGrid. This function emits Exceptioned event if an exception occurs.
The skeleton of the function is shown below.

public class NotificationController
    {
        private readonly IDmvDbHandler _dmvDbHandler;
        private readonly IBlobHandler _blobHandler;
        private readonly IOwnerNotificationHandler _ownerNotificationHandler;
        private readonly IEventHandler _eventHandler;

        public NotificationController(IDmvDbHandler dmvDbHandler,
            IBlobHandler blobHandler,
            IOwnerNotificationHandler ownerNotificationHandler,
            IEventHandler eventHandler)
        {
            _dmvDbHandler = dmvDbHandler ??
                throw new ArgumentNullException(nameof(dmvDbHandler));

            _blobHandler = blobHandler ??
                throw new ArgumentNullException(nameof(blobHandler));

            _ownerNotificationHandler = ownerNotificationHandler ??
                throw new ArgumentNullException(nameof(ownerNotificationHandler));

            _eventHandler = eventHandler ??
                throw new ArgumentNullException(nameof(eventHandler));
        }

        [FunctionName("NotifyRegisteredOwner")]
        public async Task NotifyRegisteredOwner(
            [EventGridTrigger] EventGridEvent eventGridEvent,
            ILogger logger
            )
        {
        }
    }
Enter fullscreen mode Exit fullscreen mode

ManageExceptions

This function acts as the grace for the entire solution. This function is one stop shop for managing the exceptions that occur throughout any other functions. The function copies the blob from the sourceimages container to the exception container so that some one can retrace where the failure occurred and what remedy needs to be done. The skeleton of the function is shown below.

public class ExceptionController
    {
        private IBlobHandler _blobHandler;
        private readonly BlobOptions _options;

        public ExceptionController(IBlobHandler blobHandler, 
            IOptions<BlobOptions> settings)
        {
            _blobHandler = blobHandler ??
                throw new ArgumentNullException(nameof(blobHandler));

            _options = settings.Value;

        }

        [FunctionName("ManageExeceptions")]
        public async Task ManageExceptions(
                [EventGridTrigger] EventGridEvent eventGridEvent,
                ILogger logger
            )
        {

        }
    }

Enter fullscreen mode Exit fullscreen mode

Testing

Let us check out two scenarios

1. Exception Flow

When a image without a vehicle is uploaded, it will land in the exception container.
I am uploading following image with name NoVehicleImage.png to the sourceimages.

No Vehicle

Since the Azure Function cannot detect a registration number in this picture, it emits Exceptioned event and the exception management flow copies the image to exceptionfolder as shown below.
Image Transferred to exception container

2. Working Flow

There were no fast vehicles in the vicinity of my residence. So I decided to improvise. I have tested the flow using a capture of me driving a two wheeler.
I am using following image with name 15271b93-e416-4dde-9430-4994ee9cd360.png
Test Image
And soon enough an email pops up in my inbox as shown below
Received Email
And the attachment has my face blurred out.
Blurred Image in Attachment
The blurred image is present in the blurredimages container as well.
Image in Blurred Images Container

YAY, IT WORKS
Happy

Challenges

Building event driven systems comes with the perks of loose coupling and easy replaceability of the parts as all parts do not directly communicate with each other but do so via events. This however creates multiple problems.

  1. Since there is no direct communication between the parts of the system we can not create a system map which gives a nice flow of data from one part of the system to other.
  2. There is no orchestration workflow to visualize here. This poses a major problem when debugging is required. When an event driven system is not implemented correctly, it can be a major source of worry when something bad happens in production environment. Handling exceptions in a graceful way is very important to retrace the flow of message in the system.
  3. Testing event driven systems need a penchant for patience. There are many unanticipated things that can happen when testing the systems. Since there is no orchestration engine to control the flow, often tester and developers can be seen banging their heads against the wall.

The challenges all point to one thing a event driven system absolutely needs to have a well thought out correlated logging which spans across all the working parts of the event.

Luckily, Azure Functions supports logging to Application Insights by default. Each function has access to the implementation of ILogger which provides multiple ways of logging to Application Insights. It supports Structured Logging where system specific custom properties can be logged. This solution implements a correlated logging based upon the name of the blob that is uploaded(It is GUID by the way ๐Ÿ˜‰).

The application tracks the Function where the statements are logged, custom defined event for each activity, and their status.

Following class shows the logging template and the custom logging events defined for the application.

public class LoggingConstants
    {
        public const string Template
            = "{EventDescription}{CorrelationId}{ProcessingFunction}{ProcessStatus}{LogMessage}";

        public enum ProcessingFunction
        {
            ExtractRegistrationNumber,
            DetectAndBlurFaces,
            CreateSpeedingTicket,
            NotifyRegisteredOwner,
            ManageExeceptions

        }

        public enum EventId
        {


            ExtractRegistrationNumberStarted = 101,
            ExtractRegistrationNumberFinished = 102,

            DetectAndBlurFacesStarted = 301,
            DetectAndBlurFacesFinished = 302,

            CreateSpeedingTicketStarted = 201,
            CreateSpeedingTicketFinished = 202,

            NotifyVehicleOwnerStarted = 401,
            NotifyVehicleOwnerFinished = 402,

            ManageExeceptionsStarted = 501,
            ManageExeceptionsFinished = 502


        }

        public enum ProcessStatus
        {
            Started,
            Finished,
            Failed
        }

    }
Enter fullscreen mode Exit fullscreen mode

The logging can be done in any azure function as shown in example shown below.

logger.LogInformation(
                new EventId((int)LoggingConstants.EventId.ExtractRegistrationNumberStarted),
                LoggingConstants.Template,
                LoggingConstants.EventId.ExtractRegistrationNumberStarted.ToString(),
                blobName,
                LoggingConstants.ProcessingFunction.ExtractRegistrationNumber.ToString(),
                LoggingConstants.ProcessStatus.Started.ToString(),
                "Execution Started"
                );

Enter fullscreen mode Exit fullscreen mode

Exception can be logged as

logger.LogError(
                        new EventId((int)LoggingConstants.EventId.ExtractRegistrationNumberFinished),
                        LoggingConstants.Template,
                        LoggingConstants.EventId.ExtractRegistrationNumberFinished.ToString(),
                        blobName,
                        LoggingConstants.ProcessingFunction.ExtractRegistrationNumber.ToString(),
                        LoggingConstants.ProcessStatus.Failed.ToString(),
                        "Execution Failed. Reason: Failed to extract number plate from the image"
                        );
Enter fullscreen mode Exit fullscreen mode

Once this is done in each function, we have end to end correlated logging.

Following is an example of how the correlated logging looks when results are queried in Application Insights.

Query

traces 
| sort by timestamp desc
| where customDimensions.EventId > 1
| where customDimensions.prop__CorrelationId == "{blob name goes here}" 
| order by toint(customDimensions.prop_EventId) asc
| project  Level = customDimensions.LogLevel 
           , EventId = customDimensions.EventId
           , EventDescription = customDimensions.prop__EventDescription
           , ProcessingWorkflow = customDimensions.prop__ProcessingFunction
           , CorrelationId = customDimensions.prop__CorrelationId
           , Status = customDimensions.prop__Status
           , LogMessage = customDimensions.prop__LogMessage

Enter fullscreen mode Exit fullscreen mode

Vanilla Flow

Correlated Structured Logging

Exception Tracked Gracefully

Correlated Structured Logging Exception Handled
And that is it we have a trace of what is going on in the system.

Blob Storage Cleanup

Cleaning up blob storage is an important maintainance task. If the blobs are left as they are, they can accumulate and will add to the cost of running the solution.

Blob Storage account clean up is accomplished by defining a Life Cycle Management policy on the account to Delete all blobs older than 1 days. It is implemented as shown below.

Life Cycle Management Policy

Define Policy 1
Define Policy 2

Conclusion

In this article, we discussed how to design and implement a event driven system to process images uploaded to the blob containers. This is just the basic design. Actual production system will contain many other components to create manual tasks for the employees handling the exception workflow.

Repository

The code implemented as part of this small project is available under MIT License on my GitHub Repository.

GitHub logo fullstackmaddy / speeding-infraction-management

This repository contains the code to demonstrate the serverless speeding infraction management system built entirely using Azure Stack

speeding-infraction-management

This repository contains the code to demonstrate the serverless speeding infraction management system built entirely using Azure Stack

production-deployment-pipeline

Top comments (0)