A Comprehensive Guide to Azure Functions Error Monitoring
In order to monitor for errors, you need to be able to log them!
Join the DZone community and get the full member experience.
Join For FreeAzure Functions is Microsoft's solution to serverless computing. While it actually does run on servers, the key difference here is that you aren't responsible for maintaining the function hosting environment. This is both a blessing and a curse.
On the one hand, you don't need to burden yourself with the details of the OS and web server. This frees you up to focus on what's important: developing the core functionality of your application. On the other hand, you have to relinquish some control over the execution environment. When it comes to logging application errors, this presents a challenge.
This post will focus on how to monitor your Azure Functions apps for errors. You need to be able to tell when your Azure Functions apps cause problems. And in order to monitor for errors, you need to be able to log them!
Error Monitoring With a Traditional Web App
In a web-server-hosted process, you would normally connect some kind of global logger to catch and log exceptions. You would use that same logger to log from your code. This is fairly straightforward and has become almost automatic for many developers.
For example, it's effortless to hook up the Raygun's error monitoring tool in an ASP.NET Core app. Just import the Mindscape.Raygun4Net.AspNetCore
package, add the following three lines to the Startup.cs
file, and add some minimal configuration to your appsettings.json
file.
using Mindscape.Raygun4Net.AspNetCore; // within ConfigureServices services.AddRaygun(Configuration); // within Configure app.UseRaygun();
This code will set Raygun Crash Reporting to log all unhandled exceptions. Of course, you may want to configure the middleware to add additional information per request, as explained in this guide.
When you set up a logger this way, you get the benefit of reuse. All request handlers (controllers) will use the same error logging. This is desirable because it saves us from having the same boilerplate code in every controller method.
Unfortunately, you can't inject a logger into the Azure Functions host the same way. But don't worry, we still have options.
Error Logging With Azure Functions
By default, each call to an Azure Function handler receives a logger instance that logs to Azure file storage. The logger instance is passed to the function along with the invocation.
In C# .NET Core, an Azure Function method signature looks like this:
[FunctionName("HttpTriggerCSharpThrowError")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
An instance of ILogger
is passed as an argument to the function invocation. You can use extension methods from Microsoft.Extensions.Logging to log events using the log instance.
log.LogError(...); log.LogWarning(...); log.LogInformation(...); log.LogDebug(...); // etc...
And the invocation function in JavaScript looks like this (read the docs for more info):
module.exports = async function(context, myTrigger, myInput, myOtherInput)
The context passed into the handler function has a log()
function and a log
object. For logging at different levels, you call the appropriate log functions as follows:
context.log.error( "That's an error!" ) context.log.warn( "I'm warning you!" ) context.log.info( "For your information." ) context.log.verbose( "To whom it may concern..." ) context.log( "Just saying.")
And that's how you log in Azure Functions. But where does the logged event go? And how do you use your own logger instance? That's the real trouble!
Default Azure Functions Log Location
When you create a new Azure Functions app, you'll configure its storage location. You can actually go to the "file service" in the storage account and see the directories where the logs are kept. There are actually quite a few locations with logs. The "/LogFiles/Application/Functions" directory has the host logs and the function logs.
Theoretically, since you can get to the file, you should be able to send the logs to Raygun, which is what you really want to do here. You could periodically poll the logs using a timed Azure Function, but then, you'd have to rely on file reading and parsing. But this wouldn't be ideal since you would have unnecessary complexity.
It would be best to have a queue or a stream to tap into directly. There isn't an easy way to do this in Azure Functions just yet, so let's look at the next best thing.
Explicitly Logging to Raygun
There are a number of techniques you can use to send errors to Raygun, which you want to do so Raygun can work its magic. The simplest way is to explicitly log the exceptions in a catch block. Here are the steps to get this done:
1) Add the Raygun client package to your function code.
dotnet add package Mindscape.Raygun4Net.NetCore
2) Add the code to create a client.
var raygunClient = new Mindscape.Raygun4Net.RaygunClient("YOUR_TOP_SECRET_KEY");
3) Send the exception.
This is the most basic way to send exceptions to Raygun from your Azure Functions apps. Of course, you can take things up a few levels from here. Let's take a look at some things you can do to improve on this approach.
1. Store Your Key as a Config
Azure Functions apps can run multiple functions on a single host. The host is an app service that has its own configuration, and the host config is available to your functions.
This means you can store the key in the host rather than keeping it in code. That's a better practice for security purposes.
First, you'll need to add the value to the application settings. In the Azure portal, go to Function Apps and select one. You should see Application settings in the main pane for the selected app.
Once you've clicked that link, you should be in the tab for application settings. Here, you can control various aspects of the Azure Functions app hosting environment. There's an application settings section that looks like this:
This is where you'll add an app setting. Finally, you'll need to modify your code to get the value. Modify the code that created the client to this:
var key = System.Environment.GetEnvironmentVariable(
"Raygun_ApiKey",
EnvironmentVariableTarget.Process
);
var raygunClient = new Mindscape.Raygun4Net.AspNetCore.RaygunClient(key);
Microsoft recommends using GetEnvironmentVariable
rather than the ConfigurationManager
for this purpose. Now, with the variable in your hosting environment, you're using a better approach. Of course, once you have more than one function, you'll be repeating the above code again and again. This brings up an important issue: refactoring.
2. Refactor the Log Client for Reuse
Perhaps, someday, it'll be possible to connect to the log stream or inject your own logger. Until that becomes a reality, you either need to use fragile hacks or build a wrapper to use on every function call. I suggest using the following pattern to do just that. The logging construct can be wrapped up in a reusable package if you do just a little bit of leg work.
// usual usings here using Mindscape.Raygun4Net.AspNetCore; namespace YourNameSpace { public static class YourFunctionAppClass { public static Lazy<RaygunClient> RaygunClient = new Lazy<RaygunClient>(() => MyRaygunLogger.GetRaygunClient()); [FunctionName("YourFunctionName")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest reqest, ILogger logger) { var myRaygunLogger = new MyRaygunLogger(logger, RaygunClient); return await myRaygunLogger.WithRaygunClient(async (req, log) => { string name = req.Query["name"]; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); name = name ?? data?.name; if (name == "Throw") throw new ApplicationException("Thrown on purpose"); return name != null ? (ActionResult)new OkObjectResult($"Hello, {name}") : new BadRequestObjectResult("Oops, send a 'name' please!"); }, reqest); } } }
MyRaygunLogger
is a decorator that sends exceptions to Raygun, so it can do the error tracking and alerting. Here's the code for it:
public class MyRaygunLogger : ILogger { private readonly ILogger _log; private readonly Lazy<RaygunClient> _raygunClient; public MyRaygunLogger(ILogger log, Lazy<RaygunClient> raygunClient) { _log = log; _raygunClient = raygunClient; } // factory public static RaygunClient GetRaygunClient() { var key = System.Environment.GetEnvironmentVariable( "Raygun_ApiKey", EnvironmentVariableTarget.Process ); return new RaygunClient(key); } // function wrapper public async Task<IActionResult> WithRaygunClient( Func<HttpRequest, ILogger, Task<IActionResult>> func, HttpRequest req) { try { this.LogInformation("Request started"); return await func(req, this); } catch(Exception ex) { this.LogError(0, ex, "Error"); return new StatusCodeResult(500); } finally { this.LogInformation("Request complete"); } } // ILogger public IDisposable BeginScope<TState>(TState state) { return _log.BeginScope(state); } public bool IsEnabled(LogLevel logLevel) { return _log.IsEnabled(logLevel); } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if(logLevel == LogLevel.Error || logLevel == LogLevel.Critical) { _raygunClient.Value.Send(exception); } _log.Log<TState>(logLevel, eventId, state, exception, formatter); } }
The decorator does a few things. It wraps the original logger so that you can add functionality without altering the existing behavior. I'm only logging events with "Error" or "Critical" to Raygun in this sample, but you can send all events to take full advantage of your subscription.
The main business of this class is to offer a way to wrap every function call in the code that handles boilerplate logging-before the call, after the call, and exception handling.
You'll notice I'm using Lazy
to create the Raygun client. This way, the client is only instantiated when there's an exception. You'll also notice it's being created outside the function code as a public member. Since the Azure Functions app code has to be static, this is how we can unit test. Just replace the initializer with a mock and you'll be set for unit testing.
Closing Thoughts
Using the examples in this post as a starting point, you can take things in any number of directions, depending on how sophisticated you want to be. For example, you could apply standard formatting to your messages sent to Raygun. You could send all the messages to a separate Azure Function for pre-processing before they get logged. Perhaps, you'll want to send only log levels LogLevel.Error
or higher. If you want to add timing, you can do so within the wrapper method.
The point is: now, you can send messages to Raygun, so it can report on errors in your Azure Functions along with everything else. From there, Raygun can track errors and alert your response team right away.
If you're using Azure App Services, there's an extension that does this automatically. For the time being, it doesn't work the same way with Azure Functions.
By using these methods, you can still send the errors along and keep your error reporting as seamless as ever!
Published at DZone with permission of Phil Vuollet, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments