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
- Initialize Serilog before app startup
- Use structured logging with named properties
- Never log sensitive data
- Set appropriate log levels per environment
- Implement retention policies to manage disk space