| title | Logging Configuration | ||||||
|---|---|---|---|---|---|---|---|
| category | core-features | ||||||
| order | 70 | ||||||
| keywords |
|
Documentation > Core Features > Logging Configuration
Comprehensive logging and diagnostics support for debugging DynamoDB operations, especially useful in AOT (Ahead-of-Time) compiled environments where stack traces are limited.
Oproto.FluentDynamoDb provides a lightweight logging abstraction that:
- Has zero dependencies in the core library
- Provides detailed context for every operation
- Uses runtime configuration via
FluentDynamoDbOptionsfor flexibility - Provides near-zero overhead when disabled via
NoOpLogger - Integrates seamlessly with Microsoft.Extensions.Logging
- Is AOT-compatible with no reflection
By default, the library uses a no-op logger with zero overhead:
var client = new AmazonDynamoDBClient();
var table = new ProductsTable(client, "products");
// No logging - uses NoOpLogger.Instance by default
await table.Get<Product>()
.WithKey("pk", "product-123")
.ExecuteAsync();Install the adapter package:
dotnet add package Oproto.FluentDynamoDb.Logging.ExtensionsConfigure logging using FluentDynamoDbOptions and the ToDynamoDbLogger() extension method:
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.Logging.Extensions;
using Microsoft.Extensions.Logging;
// Create logger from ILoggerFactory
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.CreateLogger<ProductsTable>().ToDynamoDbLogger());
var table = new ProductsTable(client, "products", options);
// All operations are now logged
await table.Get<Product>().WithKey("pk", "product-123").ExecuteAsync();Or use the factory extension directly:
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.ToDynamoDbLogger<ProductsTable>());
var table = new ProductsTable(client, "products", options);Implement the IDynamoDbLogger interface:
using Oproto.FluentDynamoDb.Logging;
public class ConsoleLogger : IDynamoDbLogger
{
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
public void LogTrace(int eventId, string message, params object[] args)
{
// Not logged (below Information level)
}
public void LogDebug(int eventId, string message, params object[] args)
{
// Not logged (below Information level)
}
public void LogInformation(int eventId, string message, params object[] args)
{
Console.WriteLine($"[INFO] [{eventId}] {string.Format(message, args)}");
}
public void LogWarning(int eventId, string message, params object[] args)
{
Console.WriteLine($"[WARN] [{eventId}] {string.Format(message, args)}");
}
public void LogError(int eventId, string message, params object[] args)
{
Console.WriteLine($"[ERROR] [{eventId}] {string.Format(message, args)}");
}
public void LogError(int eventId, Exception exception, string message, params object[] args)
{
Console.WriteLine($"[ERROR] [{eventId}] {string.Format(message, args)}");
Console.WriteLine($"Exception: {exception}");
}
public void LogCritical(int eventId, Exception exception, string message, params object[] args)
{
Console.WriteLine($"[CRITICAL] [{eventId}] {string.Format(message, args)}");
Console.WriteLine($"Exception: {exception}");
}
}
// Use custom logger with FluentDynamoDbOptions
var options = new FluentDynamoDbOptions()
.WithLogger(new ConsoleLogger());
var table = new ProductsTable(client, "products", options);// Program.cs or Startup.cs
builder.Services.AddLogging(logging =>
{
logging.AddConsole();
logging.AddDebug();
logging.SetMinimumLevel(LogLevel.Debug);
});
// Register table with logger using FluentDynamoDbOptions
builder.Services.AddSingleton<ProductsTable>(sp =>
{
var client = sp.GetRequiredService<IAmazonDynamoDB>();
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.ToDynamoDbLogger<ProductsTable>());
return new ProductsTable(client, "products", options);
});using Amazon.Lambda.Core;
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.Logging;
public class LambdaLogger : IDynamoDbLogger
{
private readonly ILambdaContext _context;
public LambdaLogger(ILambdaContext context)
{
_context = context;
}
public bool IsEnabled(LogLevel logLevel) => true;
public void LogInformation(int eventId, string message, params object[] args)
{
_context.Logger.LogLine($"[INFO] [{eventId}] {string.Format(message, args)}");
}
public void LogError(int eventId, Exception exception, string message, params object[] args)
{
_context.Logger.LogLine($"[ERROR] [{eventId}] {string.Format(message, args)}");
_context.Logger.LogLine($"Exception: {exception}");
}
// Implement other methods...
}
// In Lambda handler
public async Task<APIGatewayProxyResponse> FunctionHandler(
APIGatewayProxyRequest request,
ILambdaContext context)
{
var options = new FluentDynamoDbOptions()
.WithLogger(new LambdaLogger(context));
var table = new ProductsTable(_dynamoDbClient, "products", options);
// Operations are logged to CloudWatch
await table.Get<Product>().WithKey("pk", productId).ExecuteAsync();
}using Microsoft.Extensions.Logging;
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.Logging.Extensions;
// Setup logging
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddConsole()
.SetMinimumLevel(LogLevel.Debug);
});
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.ToDynamoDbLogger("DynamoDB"));
var table = new ProductsTable(client, "products", options);
// Operations are logged to console
await table.Get<Product>().WithKey("pk", "product-123").ExecuteAsync();The WithLogger() method configures logging for all DynamoDB operations:
var options = new FluentDynamoDbOptions()
.WithLogger(logger);Key characteristics:
- Returns a new
FluentDynamoDbOptionsinstance (immutable pattern) - Accepts
IDynamoDbLogger?- passnullto useNoOpLogger.Instance - Can be chained with other configuration methods
The Oproto.FluentDynamoDb.Logging.Extensions package provides extension methods to convert Microsoft.Extensions.Logging types:
// From ILogger
ILogger logger = loggerFactory.CreateLogger<MyTable>();
IDynamoDbLogger dynamoLogger = logger.ToDynamoDbLogger();
// From ILoggerFactory with type category
IDynamoDbLogger dynamoLogger = loggerFactory.ToDynamoDbLogger<MyTable>();
// From ILoggerFactory with string category
IDynamoDbLogger dynamoLogger = loggerFactory.ToDynamoDbLogger("MyCategory");Chain logging with other configuration options:
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.ToDynamoDbLogger<MyTable>())
.AddGeospatial()
.WithEncryption(encryptor);
var table = new MyTable(client, "my-table", options);The base class accepts an optional FluentDynamoDbOptions parameter:
public abstract class DynamoDbTableBase
{
// Without options - uses defaults
protected DynamoDbTableBase(IAmazonDynamoDB client, string tableName)
// With options - preferred for configuring logging and other features
protected DynamoDbTableBase(IAmazonDynamoDB client, string tableName, FluentDynamoDbOptions? options)
}Pass options to the base class:
public class ProductsTable : DynamoDbTableBase
{
public ProductsTable(IAmazonDynamoDB client, string tableName)
: base(client, tableName) { }
public ProductsTable(IAmazonDynamoDB client, string tableName, FluentDynamoDbOptions? options)
: base(client, tableName, options) { }
}await table.Query<Product>("pk = {0}", "product-123").ExecuteAsync();
// Logs:
// [Information] Executing Query on table products. KeyCondition: pk = :p0
// [Debug] Query parameters: 1 values
// [Information] Query completed. ItemCount: 5, ConsumedCapacity: 2.5var product = Product.FromDynamoDb<Product>(item);
// Logs:
// [Trace] Starting FromDynamoDb mapping for Product with 8 attributes
// [Debug] Mapping property Id from String
// [Debug] Mapping property Name from String
// [Debug] Converting Tags from String Set with 3 elements
// [Debug] Converting Metadata to Map with 5 entries
// [Trace] Completed FromDynamoDb mapping for Producttry
{
var product = Product.FromDynamoDb<Product>(invalidItem);
}
catch (DynamoDbMappingException ex)
{
// Already logged:
// [Error] Failed to convert Metadata to Map. PropertyType: Dictionary<string, string>, ElementCount: 5
// Exception: InvalidCastException...
}// Only log Information and above
builder.Services.AddLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Information);
});
// Trace and Debug logs are not emitted// Configure different levels for different categories
builder.Services.AddLogging(logging =>
{
logging.AddFilter("ProductsTable", LogLevel.Debug);
logging.AddFilter("OrdersTable", LogLevel.Information);
});See Log Levels and Event IDs for filtering by specific event types.
The library checks if logging is enabled before constructing log messages:
// Internal implementation
if (logger?.IsEnabled(LogLevel.Debug) == true)
{
// This code only runs if Debug logging is enabled
logger.LogDebug(eventId, "Mapping property {PropertyName}", propertyName);
}The default NoOpLogger has zero allocation overhead:
// NoOpLogger implementation
public bool IsEnabled(LogLevel logLevel) => false;
public void LogDebug(int eventId, string message, params object[] args) { }Use NoOpLogger.Instance (the default) for near-zero overhead logging:
// Default behavior - no logging, uses NoOpLogger.Instance
var table = new ProductsTable(client, "products");
// Explicit NoOpLogger
var options = new FluentDynamoDbOptions()
.WithLogger(NoOpLogger.Instance);
var table = new ProductsTable(client, "products", options);The NoOpLogger.IsEnabled() method always returns false, causing all logging calls to be skipped with minimal overhead. This is the recommended approach for production environments where logging is not needed.
See the Advanced Topics section for more details on environment-based configuration.
Each table instance has its own configuration, providing excellent test isolation:
[Fact]
public async Task Test_WithMockLogger()
{
var mockLogger = new MockDynamoDbLogger();
var options = new FluentDynamoDbOptions()
.WithLogger(mockLogger);
var table = new ProductsTable(client, "test-products", options);
// Test operations...
Assert.True(mockLogger.LoggedMessages.Any());
}
[Fact]
public async Task Test_WithoutLogging()
{
// No logging configured - uses NoOpLogger by default
var table = new ProductsTable(client, "test-products");
// Test operations...
}Because configuration is instance-based rather than static, tests can run in parallel without interference:
// These tests can run in parallel safely
[Fact]
public async Task Test1()
{
var options = new FluentDynamoDbOptions()
.WithLogger(logger1);
var table = new ProductsTable(client, "table1", options);
// ...
}
[Fact]
public async Task Test2()
{
var options = new FluentDynamoDbOptions()
.WithLogger(logger2);
var table = new ProductsTable(client, "table2", options);
// ...
}- Check log level: Ensure the minimum log level includes the logs you want
- Check IsEnabled: Verify your logger's
IsEnabledmethod returns true - Check logger configuration: Ensure the logger is properly configured
- Check options passed to table: Ensure
FluentDynamoDbOptionswith logger is passed to the table constructor
- Increase minimum log level: Set to
InformationorWarning - Filter by category: Configure filters for specific table classes
- Filter by event ID: See Log Levels and Event IDs
- Use NoOpLogger: Disable logging entirely for specific tables
- Use IsEnabled checks: The library already does this
- Increase minimum log level: Reduce log volume
- Use NoOpLogger: Near-zero overhead when logging is disabled
- Profile your application: Measure actual impact
- Configuration Guide - Complete configuration options
- Log Levels and Event IDs - Understand when each log level is used
- Structured Logging - Query and analyze logs effectively
- Troubleshooting Guide - Common logging issues and solutions
See Also: