Observability for .NET on AWS with Powertools
Instrument your .NET serverless applications like a pro. This guide introduces AWS Powertools for .NET, a suite of utilities that simplifies structured logging, custom metrics, and distributed tracing.
Building serverless applications is one thing; operating them effectively in production is another. When something goes wrong, you need good observability—the ability to understand what's happening inside your system. This means having high-quality logs, meaningful metrics, and the ability to trace requests as they travel through multiple services.
Implementing all of this from scratch can be a lot of work. Fortunately, for developers working with .NET on AWS, there's AWS Powertools for .NET, a suite of open-source utilities that makes instrumenting your serverless applications a breeze.
Powertools for .NET provides three core features:
- Structured Logging: Easily create structured, JSON-formatted logs that are easy to parse and query.
- Custom Metrics: Publish custom business and application metrics to CloudWatch asynchronously.
- Distributed Tracing: Capture key information as annotations and metadata in your AWS X-Ray traces.
Let's explore how to use these features in a typical .NET Lambda function.
Getting Started
First, add the necessary Powertools NuGet packages to your project:
dotnet add package AWS.Lambda.Powertools.Logging
dotnet add package AWS.Lambda.Powertools.Metrics
dotnet add package AWS.Lambda.Powertools.Tracing
1. Structured Logging
Standard Console.WriteLine
logs are unstructured and hard to search. Powertools' Logger
utility allows you to write structured JSON logs and automatically includes key context like the cold start status, Lambda request ID, and memory usage.
using AWS.Lambda.Powertools.Logging;
public class Function
{
[Logging(LogEvent = true)]
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
{
// This log will be a JSON object with the message and extra context.
Logger.LogInformation("Handler invoked.");
var orderId = request.QueryStringParameters["orderId"];
// Append additional keys to all subsequent logs in this invocation.
Logger.AppendKey("orderId", orderId);
Logger.LogInformation("Fetching order details...");
return new APIGatewayProxyResponse
{
StatusCode = 200,
Body = $"Processed order {orderId}"
};
}
}
[Logging(LogEvent = true)]
: This attribute automatically logs the incoming Lambda event and handles exceptions.Logger.LogInformation()
: Writes a structured log message.Logger.AppendKey()
: Adds a key-value pair to the logging context, which will be included in all logs written after it.
Your final CloudWatch log entry will look something like this, making it easy to query for all logs related to a specific orderId
:
{
"level": "INFO",
"message": "Fetching order details...",
"service": "my-service",
"cold_start": true,
"xray_trace_id": "...",
"orderId": "order-123"
}
2. Custom Metrics
CloudWatch provides basic metrics for your Lambda function, but you often need to capture custom business metrics, like the number of orders processed or items sold. Powertools' Metrics
utility makes this easy and efficient.
It captures metrics locally and then publishes them to CloudWatch asynchronously in the Embedded Metric Format (EMF) at the end of the function's invocation. This avoids the latency of making a PutMetricData
API call for every metric.
using AWS.Lambda.Powertools.Metrics;
public class Function
{
[Metrics(Namespace = "MyApp", Service = "Orders")]
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
{
// Add a single metric
Metrics.AddMetric("SuccessfulOrders", 1, MetricUnit.Count);
// Add dimensions to slice and dice your metrics
Metrics.AddDimension("Channel", "Web");
// ... process order ...
return new APIGatewayProxyResponse { StatusCode = 200, Body = "OK" };
}
}
[Metrics]
: This attribute handles the setup and asynchronous publishing of the metrics.Metrics.AddMetric()
: Records a new metric value.Metrics.AddDimension()
: Adds a dimension, which allows you to filter and group your metrics in CloudWatch.
3. Distributed Tracing
AWS X-Ray provides distributed tracing, but it can be hard to add your own business-specific context to the traces. Powertools' Tracing
utility simplifies this.
using AWS.Lambda.Powertools.Tracing;
public class Function
{
[Tracing(SegmentName = "OrderHandler")]
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
{
var orderId = request.QueryStringParameters["orderId"];
// Add an annotation to the X-Ray trace
Tracing.AddAnnotation("orderId", orderId);
// Add metadata to the X-Ray trace
Tracing.AddMetadata("order_details", new { ItemCount = 5, Value = 125.50 });
await ProcessOrder(orderId);
return new APIGatewayProxyResponse { StatusCode = 200, Body = "OK" };
}
[Tracing(SegmentName = "ProcessOrderSubsegment")]
private async Task ProcessOrder(string orderId)
{
// This method will appear as a subsegment in the X-Ray trace
await Task.Delay(100);
}
}
[Tracing]
: This attribute creates the main trace segment and can automatically create subsegments for decorated methods.Tracing.AddAnnotation()
: Adds an annotation to the trace. Annotations are indexed and can be used for filtering traces.Tracing.AddMetadata()
: Adds metadata to the trace. Metadata is not indexed but is useful for storing additional context.
Conclusion
Good observability is not a luxury; it's a necessity for running reliable production systems. AWS Powertools for .NET provides a simple, powerful, and low-overhead way to instrument your .NET serverless applications.
By incorporating its logging, metrics, and tracing utilities into your Lambda functions, you can gain deep insights into your application's behavior, troubleshoot issues faster, and operate your serverless workloads with confidence.