A real world Azure Function example that logs client errors in a Storage Table

Today I invested some time into getting deeper to the topic of Azure Functions. Together with the new microservices architecture, new ways of doing business logic arise. Azure Functions are small chunks of code that run in a “serverless” (you do not care about allocating new hardware resources, even when the requests to the function raise) environment and each of them is meant to do one specific job.

Apart from the great examples and templates with Functions that you can find inside the Azure Portal, I tried to think of some use cases that I would use an Azure Function for a web application and I came up with the following one:

Our application is a standard client-server web application. It is possible that the JavaScript code can run into exceptions or other non-expected errors and we want to store these exception in a data center.

On the JavaScript side we have to use the onerror global event and inside it do an Ajax POST-request to the Azure function by passing in the exception.

On the server side, we are going to write an Azure Function which receives the request, checks for its validity and then stores the exception along with the UserAgent of the client into a Storage Table. Storage Tables (NoSQL storages) are considered to be the best data storing option for log operations (store and forget operations), since they can store a massive amount of information and if needed they can return this data very fast.

Here it is important to mention, that we can use this function from multiple web applications, so that we log in a central database all the errors of our applications. This is why we also use the URL of the current application as property in the code.

The client code (I used TypeScript) is a simple page with a button which when is clicked throws an exception which is catched from onerror and then with jQuery we are doing an Ajax call to the Azure Function:

declare var $: any;

class SimpleButton {
    button: HTMLButtonElement;

    constructor(element: HTMLElement) {
        this.button = document.createElement("button");
        this.button.innerText = "Click me!";
        this.button.onclick = this.throwAnException;
        element.appendChild(this.button);
    }

    // When the button is clicked, just throw an exception, so that the onerror event reacts
    private throwAnException(this: HTMLElement, ev: MouseEvent): any {
        throw new Error();
    }
}

window.onload = () => {
    var el = document.getElementById("content");
    var button = new SimpleButton(el);
}

// The catch-all event for errors in JavaScript
window.onerror = (message: string, filename: string, lineNo: number, colNo: Number, error: Error) => {
    var exceptionObj = {
        error: {
            name: error.name,
            message: message,
            file: filename,
            row: lineNo,
            column: colNo
        },
        url: window.location.host
    };

    // In the URL replace the capital words with your Function values
    $.ajax({
        url: "https://NAME_OF_FUNCTIONS_APP.azurewebsites.net/api/NAME_OF_FUNCTION?code=FUNCTION_CODE",
        type: "POST",
        data: JSON.stringify(exceptionObj),
        processData: false,
        contentType: "application/json",
        success: (data, type, obj) => {
            console.log(type);
        },
        error: (data) => {
            console.error(data);
        }
    });
}

What’s important to mention is that you have to use a real domain name for the example to work and you will have to register this domain to the CORS table (see the following image) of the Azure Functions. If you try to test the example with a localhost URL it will not work because of the CORS policy of Azure.

Applying CORS rules for an Azure Function

Here is the HTML code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="https://code.jquery.com/jquery-2.2.4.js"
            integrity="sha256-iT6Q9iMJYuQiMWNd9lDyBUStIq/8PuOW33aOqmvFpqI="
            crossorigin="anonymous"></script>
    <script src="app.js"></script>
</head>
<body>
    <div id="content"></div>
</body>
</html>

Here is the code for the Azure Function:

#r "Microsoft.WindowsAzure.Storage"

using System.Net;
using Microsoft.WindowsAzure.Storage.Table;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, ICollector<ClientError> outTable)
{
    dynamic data = await req.Content.ReadAsAsync<object>();
    var error = data?.error;
    var url = data?.url;

    if (error == null)
    {
        return req.CreateResponse(HttpStatusCode.BadRequest);
    }

    outTable.Add(new ClientError()
    {
		/* We use the URL of the application as partition key, so that we can search 
		 * faster for the errors of a specific web applications */
        PartitionKey = url,
        RowKey = Guid.NewGuid().ToString(),
        Name = error.name,
        Message = error.message,
        File = error.file,
        Row = error.row,
        Column = error.column,
        UserAgent = req.Headers.UserAgent.ToString()
    });

    return req.CreateResponse(HttpStatusCode.Created);
}

// The POCO class has to derive from TableEntity in order to get the PartitionKey and RowKey properties
public class ClientError : TableEntity
{
    public string Name { get; set; }

    public string Message { get; set; }

    public string File { get; set; }

    public int Row { get; set; }

    public int Column { get; set; }

    public string UserAgent { get; set; }
}

and here is the configuration of the function:

{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "post"
      ],
      "authLevel": "function"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "type": "table",
      "name": "outTable",
      "tableName": "THE_NAME_OF_THE_TABLE",
      "connection": "THE_STORAGE_NAME",
      "direction": "out"
    }
  ],
  "disabled": false
}

That’s it. Drop me a line if you have any questions concerning Azure Functions or Azure Services in general.

comments powered by Disqus