Implementing Serilog Structured Logging in Optimizely CMS 12

Introduction

This guide shows you how to implement Serilog - a powerful structured logging library - in Optimizely CMS 12.

What you'll learn:

  • Configure Serilog with file and console outputs
  • Use structured logging in your code
  • View and manage log files
  • Best practices and troubleshooting

Prerequisites:

  • Optimizely CMS 12.x on .NET 6.0+
  • Basic ASP.NET Core knowledge

Why Serilog?

✅ Structured Logging - Logs are structured data, not just text
✅ Multiple Outputs - Console, files, databases simultaneously
✅ High Performance - Asynchronous logging
✅ Easy Configuration - JSON-based settings


Step 1: Install Packages

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Step 2: Initialize in Program.cs

using Serilog;

public class Program
{
    public static void Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build())
            .CreateLogger();

        try
        {
            Log.Information("Starting Optimizely CMS");
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Application start-up failed");
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog()
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Step 3: Configure appsettings.json

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-log-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "fileSizeLimitBytes": 10485760,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
          "restrictedToMinimumLevel": "Error"
        }
      },
      {
        "Name": "Console",
        "Args": {
          "restrictedToMinimumLevel": "Information"
        }
      }
    ]
  }
}

Key Settings:

Setting Value Description
rollingInterval Day New file each day
retainedFileCountLimit 30 Keep 30 days
fileSizeLimitBytes 10485760 (10MB) Max file size
File Level Error Only errors to file
Console Level Information All info to console

Step 4: Use in Your Code

Basic Logging

public class ProductController : Controller
{
    private readonly ILogger<ProductController> _logger;

    public ProductController(ILogger<ProductController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index(int productId)
    {
        _logger.LogInformation("Loading product {ProductId}", productId);

        try
        {
            var product = LoadProduct(productId);
            return View(product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load product {ProductId}", productId);
            return NotFound();
        }
    }
}

Structured Logging

// ❌ BAD: String interpolation
_logger.LogInformation($"User {userId} created order {orderId}");

// ✅ GOOD: Structured properties
_logger.LogInformation(
    "User {UserId} created order {OrderId} with total {Total:C}",
    userId, orderId, total);

Log Levels

_logger.LogTrace("Detailed tracing");        // Development only
_logger.LogDebug("Debug information");       // Development
_logger.LogInformation("Normal events");     // Production
_logger.LogWarning("Potential issues");      // Production
_logger.LogError(ex, "Errors");              // Production
_logger.LogCritical("Critical failures");    // Production

Viewing Logs

Option 1: Console (Development)

When running dotnet run, all logs appear in real-time in the console.

Pros: ✅ Real-time, ✅ All levels
Cons: ❌ Not persistent

Option 2: Log Files (Production)

Files saved at: /logs/app-log-20260120.txt

Pros: ✅ Persistent, ✅ Only errors
Cons: ❌ Not real-time

Option 3: Custom Admin Tool

Create a simple log viewer in CMS admin:

[Authorize(Roles = "CmsAdmins")]
public class LogViewerController : Controller
{
    private readonly IWebHostEnvironment _environment;

    public LogViewerController(IWebHostEnvironment environment)
    {
        _environment = environment;
    }

    [Route("admin/logs")]
    public IActionResult Index()
    {
        var logDir = Path.Combine(_environment.ContentRootPath, "logs");
        var files = Directory.GetFiles(logDir, "*.txt")
            .Select(f => new FileInfo(f))
            .OrderByDescending(f => f.LastWriteTime)
            .Select(f => new {
                Name = f.Name,
                Size = $"{f.Length / 1024} KB",
                Modified = f.LastWriteTime
            });

        return View(files);
    }

    [Route("admin/logs/download/{fileName}")]
    public IActionResult Download(string fileName)
    {
        var logDir = Path.Combine(_environment.ContentRootPath, "logs");
        var filePath = Path.Combine(logDir, fileName);
        
        if (!System.IO.File.Exists(filePath))
            return NotFound();

        var content = System.IO.File.ReadAllBytes(filePath);
        return File(content, "text/plain", fileName);
    }
}

Best Practices

1. Use Structured Logging

// ✅ Always use named properties
_logger.LogInformation("Processing {ItemCount} items", items.Count);

2. Never Log Sensitive Data

Never log:

  • ❌ Passwords
  • ❌ Credit cards
  • ❌ API keys
  • ❌ Personal information

3. Choose Appropriate Levels

  • Debug/Trace → Development only
  • Information → Business events
  • Warning → Potential issues
  • Error → Exceptions
  • Critical → Application crashes

4. Use Conditional Logging

if (_logger.IsEnabled(LogLevel.Debug))
{
    var expensiveData = ComputeExpensiveData();
    _logger.LogDebug("Debug: {Data}", expensiveData);
}

5. Add Context with Scopes

using (_logger.BeginScope("OrderId={OrderId}", orderId))
{
    _logger.LogInformation("Validating order");
    _logger.LogInformation("Processing payment");
    // All logs include OrderId
}

Common Issues

Issue 1: No Logs in Files

Problem: Console shows logs but files are empty

Solution:

// Check restrictedToMinimumLevel is not too high
{
  "Args": {
    "restrictedToMinimumLevel": "Information"  // Lower this
  }
}

Issue 2: Files Too Large

Problem: Log directory consuming too much space

Solution:

{
  "Serilog": {
    "WriteTo": [{
      "Args": {
        "rollingInterval": "Day",
        "retainedFileCountLimit": 7,        // Keep only 7 days
        "fileSizeLimitBytes": 5242880,      // 5MB max
        "restrictedToMinimumLevel": "Warning"  // Only warnings+
      }
    }]
  }
}

Issue 3: Admin Tool Shows No Files

Problem: Path mismatch between Serilog and admin tool

Solution:

// Ensure paths match
{
  "Serilog": {
    "WriteTo": [{
      "Args": {
        "path": "logs/app-log-.txt"  // ← Must match tool path
      }
    }]
  }
}

Verify in code:

var logPath = Path.Combine(_environment.ContentRootPath, "logs");
if (!Directory.Exists(logPath))
{
    _logger.LogWarning("Log directory not found: {Path}", logPath);
}

Issue 4: Performance Impact

Problem: Application slow with logging

Solution:

// 1. Limit log levels in production
"MinimumLevel": { "Default": "Information" }

// 2. Use async file sink
services.AddSerilog(config => 
    config.WriteTo.Async(a => a.File("logs/app.txt")));

// 3. Conditional logging for expensive operations
if (_logger.IsEnabled(LogLevel.Debug))
{
    _logger.LogDebug("Expensive: {Data}", ComputeData());
}

Environment-Specific Configuration

appsettings.Development.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Verbose"
    }
  }
}

appsettings.Production.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Warning"
    }
  }
}

Quick Reference

Log Level Guidelines

Level When Example
Trace Detailed tracing Cache lookup
Debug Internal events Query results
Information Normal flow Order created
Warning Potential issues Slow response
Error Exceptions Payment failed
Critical App crash Database down

Common Patterns

// Simple logging
_logger.LogInformation("User logged in");

// With properties
_logger.LogInformation("Order {OrderId} total {Total:C}", id, total);

// With exception
_logger.LogError(ex, "Failed to process {OrderId}", id);

// With scope
using (_logger.BeginScope("UserId={UserId}", userId))
{
    // All logs include UserId
}

// Conditional
if (_logger.IsEnabled(LogLevel.Debug))
{
    _logger.LogDebug("Data: {Data}", expensiveData);
}

 

Conclusion

Serilog in Optimizely CMS 12 provides:

✅ Structured logging for better analysis
✅ Multiple outputs (console + files)
✅ Flexible configuration via JSON
✅ Production-ready with file rotation

Key Takeaways

  1. Initialize Serilog before app startup
  2. Use structured logging with named properties
  3. Never log sensitive data
  4. Set appropriate log levels per environment
  5. Implement retention policies to manage disk space
← Back to Blog