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!
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
- The cameras upload captured images to the cloud storage as blobs along with all the details like the district where infraction occurred etc.
- 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.
The logical flow of the control in the system can be represented as below.
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
- Analyze images using pre-defined and trained models and extract rich information from the image.
- Perform OCR which enables us to extract printed and handwritten text from the images with a very high accuracy
- Recognize famous personalities, places landmarks etc.
- 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
- Detect faces
- 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
- 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.
- 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:
- 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.
- 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:
- We will tap into the "Blob Created" event emitted when the images are uploaded to blob container. This event will kick start the flow.
- 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:
- The system managing the camera and uploads to the cloud will upload the images to the
sourceimages
container. - The Serverless system will detect faces and upload the images with blurred faces to the
blurredimages
container. - 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
- .NET Core 3.1 as the Azure Functions project targets this framework ( Download .NET Core 3.1 )
- Azure Functions core tools (Work with Azure Functions Core Tools)
- Visual Studio Code or Visual Studio 2019 or any IDE or Editor capable of running .NET core applications.
Optional:
- Azure Storage Explorer(Download)
- Azure CosmosDB Emulator (Work locally with Azure CosmosDB)
NuGet Packages
Following screenshot shows all the nuget packages required in solution.
Solution Structure
Once set up the solution looks like following.
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);
}
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);
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);
}
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);
}
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);
}
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; }
}
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"
}
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);
}
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; }
}
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":""
}
}
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)
{
}
}
}
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);
});
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")
)
);
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>();
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
)
{
}
}
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
}
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
)
{
}
}
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
)
{
}
}
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
)
{
}
}
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
)
{
}
}
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
.
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.
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
And soon enough an email pops up in my inbox as shown below
And the attachment has my face blurred out.
The blurred image is present in the blurredimages
container as well.
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.
- 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. - 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.
- 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
}
}
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"
);
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"
);
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
Vanilla Flow
Exception Tracked Gracefully
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.
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.
Top comments (0)