Structured logging in Azure Functions can dramatically improve your ability to debug and monitor your applications.
Here’s an Azure Function that writes structured logs using ILogger:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Company.Function
{
public class HttpTriggerCSharp
{
private readonly ILogger<HttpTriggerCSharp> _logger;
public HttpTriggerCSharp(ILogger<HttpTriggerCSharp> logger)
{
_logger = logger;
}
[Function("HttpTriggerCSharp")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
if (name != null)
{
// Structured logging example: logging with properties
_logger.LogInformation("Found name in query string or request body: {Name}", name);
// Logging with custom properties and a scope
using (_logger.BeginScope("ProcessingUser:{UserName}", name))
{
_logger.LogInformation("Processing data for user {UserName}.", name);
// Simulate some work
await Task.Delay(100);
_logger.LogInformation("Finished processing for user {UserName}.", name);
}
return new OkObjectResult($"Hello, {name}. This HTTP triggered function executed successfully.");
}
else
{
// Logging an error with structured data
_logger.LogError("Name parameter not found in request. Query: {Query}, Body: {Body}", req.QueryString, requestBody);
return new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
}
}
The real magic happens when you look at how these logs appear in Azure Monitor Logs (Log Analytics). Instead of just a wall of text, you get distinct fields. For the _logger.LogInformation("Found name in query string or request body: {Name}", name); line, the log entry in Log Analytics will have a Message field like "Found name in query string or request body: Alice" and a separate Name field with the value "Alice". This is because the {Name} placeholder in the log message is recognized as a structured property.
When you use _logger.BeginScope("ProcessingUser:{UserName}", name), you’re not just logging a message; you’re creating a scope. Any logs generated within that using block will automatically have the UserName property (in this case, "Alice") appended to them. This is incredibly powerful for tracing a single request or operation across multiple log entries. You can query for all logs related to a specific user or transaction by filtering on UserName in Log Analytics.
The ILogger interface in .NET Core and .NET 6+ (which Azure Functions v3 and v4 use) is designed for structured logging. When you use string interpolation with named parameters like {Name} or {UserName}, the logging provider (configured by the Functions host) parses these placeholders and treats the corresponding arguments as distinct properties. This is the default behavior and doesn’t require any special setup beyond using the ILogger correctly.
The underlying mechanism relies on the Microsoft.Extensions.Logging abstraction. The Azure Functions host injects an ILogger instance, and its providers (like the one sending logs to Application Insights) are responsible for serializing these structured properties. By default, for Application Insights, these properties are sent as custom dimensions or as part of the customDimensions JSON blob. This makes them searchable and filterable in the customEvents or traces tables in Log Analytics.
Most developers miss that the ILogger itself is already doing the heavy lifting for structured logging when you use its templated methods. You don’t need a separate library for basic structured logging. The key is understanding that {Placeholder} syntax in your ILogger messages isn’t just for readability; it’s the explicit signal to the logging provider to extract that value as a structured field.
If you need to log complex objects, you’d typically serialize them to JSON first and then log the JSON string, or use a logging provider that explicitly supports complex object serialization as structured properties, though the default ILogger with simple types is often sufficient.
The next logical step is to explore how to create custom logging providers or sinks for more advanced scenarios, like sending logs to a different destination or transforming them before they leave your function.