Build a Serverless Link Shortener with Analytics Faster than Finishing your Latte
How to leverage Azure Functions, Azure Table Storage, and Application Insights to build a serverless custom URL shortening tool.
Part of the series: Serverless Link Shortener
Our team thrives on real world data. I continuously analyze my online presence to better understand what topics people are interested in so that I can focus on curating content that will drive value. A lot of what I share goes through social media feeds like Facebook, Google+ (yes, itâs still around), LinkedIn and of course Twitter. I canât afford to take a âfire and forgetâ approach or I could end up sharing content and topics no one really cares about.
Although there are plenty of freely available online URL shortening and tracking utilities, I was frustrated with my options. Today, Twitter doesnât count the full link size you tweet against your 140 character budget, so itâs not really about making the links short. Instead, itâs more about tagging and tracking. I tag links so that know which medium is the most effective. Most freely available tools require me to painstakingly paste each variation of the link in order to get a short URL, and then I donât have full control over the analytics. Whatâs more, the scheduling tool that promises to shorten URLs automatically ends up taking over the tracking tags so my data is corrupted.
I decided to build a tool of my own, but I didnât want to spin up VMs and configure expensive infrastructure to handle the load of a ton of redirects going through my servers. So, I decided to go serverless: the perfect task for <âĄ> Azure Functions to take on!
This post walks through how the entire site was set up, but you donât have to manually repeat the same steps. Instead, you can navigate directly to my GitHub repository:
JeremyLikness/serverless-url-shortener
Click on the Deploy to Azure button and you will be prompted to fill out a few values before the template engine creates and configures a fully functioning serverless app for you! Hereâs a quick video that demonstrates the full process.
To get started, I simply added a new function app from the portal and checked the box for Application Insights. This gives me all of the data and metrics Iâll ever need or hope for with minimal effort. For storage, I chose to go with Azure Table Storage. Itâs a key/value store and is perfect for matching the key (short URL) to the value (long URL). It does its job fast!
As you can see from the chart, even the âslowâ operations finish in a few hundred milliseconds and the vast majority are fasterâââcloser to six milliseconds.
Shortening the URL
The first function to build is the URL shortening code. Itâs a good idea to keep this function secure so not just anyone can make new links. I created an HTTP binding to make it straightforward to hook up a small web app utility to generate the links, with an input binding for table storage.
The parameters to the input binding automatically bring in the latest key to generate a new id for the shortening utility. I also created an output binding to write the new key out. This is the full bindings file for the âingestâ function that takes a long URL and makes a short one:
{
"bindings": [
{
"type": "table",
"name": "keyTable",
"tableName": "urls",
"partitionKey": "1",
"rowKey": "KEY",
"take": 1,
"connection": "AzureWebJobsStorage",
"direction": "in"
},
{
"type": "table",
"name": "tableOut",
"tableName": "urls",
"connection": "AzureWebJobsStorage",
"direction": "out"
},
{
"type": "httpTrigger",
"name": "req",
"authLevel": "function",
"direction": "in"
},
{
"type": "http",
"name": "$return",
"direction": "out"
}
],
"disabled": false
}
Notice that Iâm reusing the existing storage credentials created for the functionâââno need to create a new storage instance. The nice thing about table storage is that you also donât have to worry about creating the table, because it will be automatically built the first time you insert a value. You can map tables to strongly typed classes, so I created a common âmodels.csxâ file to use in the project.
using Microsoft.WindowsAzure.Storage.Table;
public class NextId : TableEntity
{
public int Id { get; set; }
}
public class ShortUrl : TableEntity
{
public string Url { get; set; }
public string Medium { get; set; }
}
public class Request
{
public bool? TagSource { get; set; }
public bool? TagMediums { get; set; }
public string Input { get; set; }
}
public class Result
{
public string ShortUrl { get; set; }
public string LongUrl { get; set; }
}
The âNextIdâ is a special entity used to keep track of the short URL. I simply increment the id and use an algorithm to turn it into a smaller alphanumeric value. I didnât see the need for any exotic hash algorithms or unique generators when I can just use a simple identity field that is incremented each time. The âShortUrlâ holds the URL and the âMediumâ property (i.e. did this link come from Twitter, LinkedIn, etc.). Both are based on âTableEntityâ that provides some basic fields including âPartitionKeyâ and âRowKey.â
For the next-up identifier (âNextIdâ) I hard coded a partition of â1â and a row of âKEYâ to always grab a single value. For the short URLs, I partition the data by the first character of the short URL. For example, a value of âabâ will go to partition âaâ and row âabâ and a value of â1zâ will go to partition â1â and row â1zâ. This ensures an even distribution across partitions with values for performance.
The algorithm to generate a short URL simply takes the integer identifier and converts it into an alphanumeric value that is based on using all digits and alphabet values.
public static readonly string Alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
public static readonly int Base = Alphabet.Length;
public static string Encode(int i)
{
if (i == 0)
{
return Alphabet[0].ToString();
}
var s = string.Empty;
while (i > 0)
{
s += Alphabet[i % Base];
i = i / Base;
}
return string.Join(string.Empty, s.Reverse());
}
The code for the function first casts the request to a strongly typed class and ensures all values are present. Note in the parameters that because the input binding for the table is set to retrieve a single value, we can pass in a strongly typed model for it (âkeyTableâ). The output table requires some additional operations and is cast to âCloudTable.â This type of automatic binding makes functions extremely flexible and easy to use.
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, NextId keyTable, CloudTable tableOut, TraceWriter log)
{
if (req == null)
{
return req.CreateResponse(HttpStatusCode.NotFound);
}
Request input = await req.Content.ReadAsAsync<Request>();
if (input == null)
{
return req.CreateResponse(HttpStatusCode.NotFound);
}
var url = input.Input;
bool tagMediums = input.TagMediums.HasValue ? input.TagMediums.Value : true;
bool tagSource = (input.TagSource.HasValue ? input.TagSource.Value : true) || tagMediums;
if (String.IsNullOrWhiteSpace(url))
{
throw new Exception("Need a URL to shorten!");
}
}
The first time the function is called, the key doesnât exist so it is passed in as null. On subsequent operations the current value will be automatically bound and passed in. A little logic checks for a null key and seeds the first value.
if (keyTable == null)
{
keyTable = new NextId
{
PartitionKey = "1",
RowKey = "KEY",
Id = 1024
};
var keyAdd = TableOperation.Insert(keyTable);
await tableOut.ExecuteAsync(keyAdd);
}
The new URL is generated based on the ânext upâ key, and stored in the table. The result is collected in a list that is returned to the client in order to display the shortened URLs. If âtag mediumsâ is set to âtrueâ the code will iterate through an array of mediums (Twitter, LinkedIn, etc.) and generate URLs for each variation.
var shortUrl = Encode(keyTable.Id++);
var newUrl = new ShortUrl
{
PartitionKey = $"{shortUrl.First()}",
RowKey = $"{shortUrl}",
Url = url
};
var singleAdd = TableOperation.Insert(newUrl);
await tableOut.ExecuteAsync(singleAdd);
result.Add(new Result
{
ShortUrl = $"{SHORTENER_URL}{newUrl.RowKey}",
LongUrl = WebUtility.UrlDecode(newUrl.Url)
});
The final step is to save the new key value to the database and return the results.
Because is this is a âpersonal useâ URL shortening tool, Iâm not worried about concurrency (i.e. what if two clients request short URLs at the exact same time) or I might code the key logic differently.
var operation = TableOperation.Replace(keyTable);
await tableOut.ExecuteAsync(operation);
return req.CreateResponse(HttpStatusCode.OK, result);
Thatâs it! You can now pass in a value and receive the short URL. This is an example of what a request/response looks like:
{
"req": {
"tagSource": false,
"tagMediums": true,
"input": "https://blog.jeremylikness.com/"
},
"response": [{
"ShortUrl":"https://jlik.me/az52",
"LongUrl":"https://blog.jeremylikness.com/?utm_medium=twitter"
}, {
"ShortUrl":"https://jlik.me/az53",
"LongUrl":"https://blog.jeremylikness.com/?utm_medium=linkedin"
}]
}
A Simple Client
To make it easier to shorten URLs, I created a simple single page web application using the Vue.js framework. Because it is just for personal use, I didnât bother with a full web application or complex authentication. Instead, the app has a hard-coded endpoint to the function with the function secret embedded in the URL. The app simply accepts input, calls the function app and displays the results.
Use this link to browse the source code for the web app. It includes a Dockerfile that builds a tiny docker image that I run locally when Iâm using it to post URLs. I keep the Docker image local and run it when needed. Because the function to generate short URLs is secured, I added a Cross-Origin Resource Sharing entry to the function app so the browser will allow requests from âlocalhostâ and a custom port. Use this link to: Learn how to configure CORS.
Redirection and Custom Telemetry
The next step is to provide an endpoint for the redirection. This is done with the âUrlRedirectâ function. The route is set up so that the short URL is passed as part of the path. The function itself accepts an input binding to the table and the short URL itself.
The logic is simple. A fallback URL is used to redirect to a common website if the URL is invalid. Table storage is queried for the matching entry, and if it is found the redirect URL is updated from the default to the URL from the table. Finally, the method returns a â302â redirect code with the target URL.
public static HttpResponseMessage Run(HttpRequestMessage req, CloudTable inputTable,
string shortUrl, TraceWriter log)
{
var redirectUrl = FALLBACK_URL;
if (!String.IsNullOrWhiteSpace(shortUrl))
{
shortUrl = shortUrl.Trim().ToLower();
var partitionKey = $"{shortUrl.First()}";
TableOperation operation = TableOperation.Retrieve<ShortUrl>(partitionKey, shortUrl);
TableResult result = inputTable.Execute(operation);
ShortUrl fullUrl = result.Result as ShortUrl;
if (fullUrl != null)
{
redirectUrl = WebUtility.UrlDecode(fullUrl.Url);
}
}
var res = req.CreateResponse(HttpStatusCode.Redirect);
res.Headers.Add("Location", redirectUrl);
return res;
}
Now all of the pieces are in place: a function to encode short URLs and a function to decode and redirect. Earlier I mentioned checking a box for âApplication Insightsâ when creating the function. Application Insights provides a ton of metrics âout of the boxâ but really shines in its ability to track custom telemetry.
Application Insights
Application insights provides rich functionality out of the box without changing a single line of code. I can see the overall health of my functions including average response time.
You can see there was a slow spike at one point that I can investigate by clicking on the spike and drilling into individual transactions. The majority of redirects happen quickly. âSmart detectionâ uses machine learning to scan data and notify you when behaviors falls outside of norms, such as failed responses or extremely slow response times. In this snapshot, the host allocated a second server to accommodate requests based on request rates and response times. I didnât have to do a thingâââthe functions scale themselves automatically.
Of course, seeing how long it takes to return a request is important, but I need more data to troubleshoot why slower responses happen. For example, it would be great to see how much time in a request is spent looking up the redirect URL in table storage.
Adding custom telemetry is very straightforward. I added a âproject.jsonâ file to reference the Application Insights SDK for use by the function.
{
"frameworks": {
"net46":{
"dependencies": {
"Microsoft.ApplicationInsights": "2.2.0"
}
}
}
}
Upon saving that, the function app loads the NuGet package and installs it to be available to the function. At the top of the function app I use a special keyword for package references â#râ to include the NuGet package followed by a standard âusingâ statement for the code.
#r "Microsoft.WindowsAzure.Storage"
using Microsoft.ApplicationInsights;
With that, I am able to instantiate a client in the function for use. Notice that I pass it a key that is already configured in the application settings to connect to the right instance of Application Insights.
public static TelemetryClient telemetry = new TelemetryClient()
{
InstrumentationKey =
System.Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY")
};
Next, I capture the current date and start a timer before calling the table operation. After it completes, I use the telemetry client to log the results.
var startTime = DateTime.UtcNow;
var timer = System.Diagnostics.Stopwatch.StartNew();
TableOperation operation = TableOperation.Retrieve<ShortUrl>(
partitionKey, shortUrl);
TableResult result = inputTable.Execute(operation);
telemetry.TrackDependency("AzureTableStorage", "Retrieve",
startTime, timer.Elapsed, result.Result != null);
This allows me to view the dependency data in Application Insights and see that the table queries happen very fastâââat the 50th percentile in less than seven milliseconds for an overall average of about 15ms!
For my own analytics, I capture a custom event to track the medium used and a âpage viewâ for the target URL.
telemetry.TrackEvent(fullUrl.Medium);
telemetry.TrackPageView(redirectUrl);
This enables me to see which mediums are the most popular.
I can also see which page views have been the most popular and learn what my audience is interested in and likely wants to see more of.
The analytics tool is actually very comprehensive and allows you to build your own queries and aggregations and create your own charts and graphs for insights. All of this is part of the free âout of the boxâ insights offering. The only thing limited in the free version is the amount of data that is stored. You can opt-in to paid models that retain more data for a longer period of time to get monthly, yearly, and other trends as needed.
The Finishing Touches: Proxies and Hosts
There are a few steps I took to make my URL shortening tool production-ready. First, a URL like this:
âhttp://hostname.azurewebsites.net/api/UrlRedirect/aaaâ
is hardly short! Therefore, I used a proxy to map the root path or route to the API. Proxy configuration is a powerful feature for functions because it enables precise control over the endpoints of your APIs. Even if you implement five different function apps with different URLs, you can use a proxy to aggregate them under the same path. Here is the proxy configuration that is setup for this project:
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"Domain Redirect": {
"matchCondition": {
"route": "/{*shortUrl}"
},
"backendUri": "http://%WEBSITE_HOSTNAME%/api/UrlRedirect/{shortUrl}"
},
"Api": {
"matchCondition": {
"route": "/api/{*path}"
},
"backendUri": "http://%WEBSITE_HOSTNAME%.azurewebsites.net/api/{path}"
}
}
}
The first rule flattens the root redirect, so that:
âhttp://hostname.azurewebsites.net/abcâ
maps to:
âhttp://hostname.azurewebsites.net/api/UrlRedirect/abcâ
The second rule ensures the original âapiâ path is preserved, so that the full path is still valid and essentially âpasses through.â
I acquired a domain name to use for the short URL. One of the settings for the function app is âCustom Domains.â
The subsequent dialog will walk you through the steps needed to verify you own the domain and point it to your function app by pointing your domain to the public IP address Azure configured. After the step is completed, the function app recognizes the custom domain and allows the redirect to work with a custom URL:
There are several options to secure your endpoint with SSL. If you own your own SSL certificate, you can upload it to the site and leverage it for secure calls. I personally prefer to use CloudFlare as a free and easy to configure option. CloudFlare allows me to configure my domain for secured connections and generates their own SSL certificate. CloudFlare connects to my Azure function app âbehind the scenesâ but presents a valid certificate to the client that secures the connection. It also offers various security features and caches requests to improve the responsive of the site. It will even serve cached content when your website goes down!
Now the URL shortening tool is fully up and running, allowing me to generate short URLs and happily routing users to their final destination. Now is the perfect time to mention one of the biggest benefits I received by going serverless: cost. As of this writing, the Azure Functions pricing model grants the first million function calls and 400,000 gigabyte seconds (GB-s) of consumption for free. My site has low risk of hitting those levels, so my main cost is storage. How much is storage?
Update: I recently spent thirty minutes in an interview discussing and demoing the link shortener that I use. Check that out here:
Azure Functions: Less-Server and More Code
Although actual results will differ for everyone, in my experience, running the site for a week while generating around 1,000 requests per day resulted in a massive seven cent U.S.D. charge to my bill. I donât think Iâll have any problem affording this!
What are you waiting for? Head over to the repository and click the button to get started on your own to see just how powerful functions are!
Read the next article in this series:
Serverless data platform CosmosDB meets serverless compute platform Azure Functions in this example based on a URL link shortener that is used to track click-through metrics.
Part of the series: Serverless Link Shortener
- Build a Serverless Link Shortener with Analytics Faster than Finishing your Latte
- Real-Time Insights with Real-Low Effort
- Expanding Azure Functions to the Cosmos
- Exploring the CosmosDB with Power BI
- Serverless Twitter Analytics with CosmosDB and Logic Apps