Skip to content

Latest commit

 

History

History
510 lines (376 loc) · 14.2 KB

File metadata and controls

510 lines (376 loc) · 14.2 KB
title Logging Configuration
category core-features
order 70
keywords
logging
diagnostics
debugging
IDynamoDbLogger
Microsoft.Extensions.Logging
FluentDynamoDbOptions

Documentation > Core Features > Logging Configuration

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.

Overview

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 FluentDynamoDbOptions for flexibility
  • Provides near-zero overhead when disabled via NoOpLogger
  • Integrates seamlessly with Microsoft.Extensions.Logging
  • Is AOT-compatible with no reflection

Quick Start

No Logger (Default Behavior)

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();

With Microsoft.Extensions.Logging

Install the adapter package:

dotnet add package Oproto.FluentDynamoDb.Logging.Extensions

Configure 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);

With Custom Logger

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);

Configuration Examples

ASP.NET Core Application

// 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);
});

AWS Lambda Function

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();
}

Console Application

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();

FluentDynamoDbOptions Configuration

WithLogger() Method

The WithLogger() method configures logging for all DynamoDB operations:

var options = new FluentDynamoDbOptions()
    .WithLogger(logger);

Key characteristics:

  • Returns a new FluentDynamoDbOptions instance (immutable pattern)
  • Accepts IDynamoDbLogger? - pass null to use NoOpLogger.Instance
  • Can be chained with other configuration methods

ToDynamoDbLogger() Extension 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");

Combining with Other Features

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);

Table Constructor Signature

DynamoDbTableBase

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)
}

Custom Table Classes

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) { }
}

What Gets Logged

Operation-Level Logging

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.5

Mapping-Level Logging

var 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 Product

Error Logging

try
{
    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...
}

Filtering Logs

By Log Level

// Only log Information and above
builder.Services.AddLogging(logging =>
{
    logging.SetMinimumLevel(LogLevel.Information);
});

// Trace and Debug logs are not emitted

By Category

// Configure different levels for different categories
builder.Services.AddLogging(logging =>
{
    logging.AddFilter("ProductsTable", LogLevel.Debug);
    logging.AddFilter("OrdersTable", LogLevel.Information);
});

By Event ID

See Log Levels and Event IDs for filtering by specific event types.

Performance Considerations

IsEnabled Check

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);
}

No-Op Logger Overhead

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) { }

Disabling Logging (Zero Overhead)

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.

Test Isolation

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...
}

Parallel Test Support

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);
    // ...
}

Troubleshooting

No Logs Appearing

  1. Check log level: Ensure the minimum log level includes the logs you want
  2. Check IsEnabled: Verify your logger's IsEnabled method returns true
  3. Check logger configuration: Ensure the logger is properly configured
  4. Check options passed to table: Ensure FluentDynamoDbOptions with logger is passed to the table constructor

Too Many Logs

  1. Increase minimum log level: Set to Information or Warning
  2. Filter by category: Configure filters for specific table classes
  3. Filter by event ID: See Log Levels and Event IDs
  4. Use NoOpLogger: Disable logging entirely for specific tables

Performance Impact

  1. Use IsEnabled checks: The library already does this
  2. Increase minimum log level: Reduce log volume
  3. Use NoOpLogger: Near-zero overhead when logging is disabled
  4. Profile your application: Measure actual impact

Next Steps


See Also: