Logging
Master Scryber’s logging system to diagnose issues, monitor performance, and troubleshoot PDF generation in development and production.
Learning Objectives
By the end of this article, you’ll be able to:
- Configure Scryber logging
- Use different log levels appropriately
- Implement custom log handlers
- Analyze log output for troubleshooting
- Configure logging for production
- Monitor PDF generation performance
Log Levels
Scryber provides several logging levels:
using Scryber.Logging;
public enum TraceRecordLevel
{
Off, // No logging
Errors, // Only errors
Warnings, // Errors and warnings
Messages, // Errors, warnings, and informational messages
Verbose // Everything (debugging)
}
Basic Logging
Console Logging
using Scryber.Components;
using Scryber.Logging;
using System.IO;
// Create document
var doc = Document.ParseDocument("template.html");
// Enable console logging
doc.TraceLog = new PDFTraceLog();
doc.TraceLog.SetRecordLevel(TraceRecordLevel.Messages);
// Generate PDF
using (var stream = new FileStream("output.pdf", FileMode.Create))
{
doc.SaveAsPDF(stream);
}
// Output goes to console:
// [INFO] Document parsing started
// [INFO] Loading template: template.html
// [INFO] Rendering page 1
// [INFO] Document generated successfully
Collector Logging
using Scryber.Logging;
// Collect log entries for analysis
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Verbose);
doc.TraceLog = logger;
// Generate document
doc.SaveAsPDF(stream);
// Analyze collected entries
foreach (var entry in logger.Entries)
{
Console.WriteLine($"[{entry.Level}] {entry.Category}: {entry.Message}");
if (entry.Level == TraceRecordLevel.Errors)
{
// Handle errors
HandleError(entry);
}
}
Log Level Selection
Development
// Development: Log everything
doc.TraceLog = new PDFCollectorTraceLog(TraceRecordLevel.Verbose);
Use Verbose when:
- Debugging issues
- Understanding document generation flow
- Learning Scryber behavior
- Investigating performance problems
Production
// Production: Log warnings and errors only
doc.TraceLog = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
Use Warnings when:
- Running in production
- Want to know about potential issues
- Avoid excessive log volume
- Monitor system health
Performance-Critical
// Performance-critical: Errors only
doc.TraceLog = new PDFCollectorTraceLog(TraceRecordLevel.Errors);
// Or disable logging entirely
doc.TraceLog = new PDFTraceLog();
doc.TraceLog.SetRecordLevel(TraceRecordLevel.Off);
Custom Log Handlers
File Logging
using Scryber.Logging;
using System;
using System.IO;
public class FileTraceLog : PDFTraceLog
{
private readonly string _logFilePath;
private readonly object _lock = new object();
public FileTraceLog(string logFilePath, TraceRecordLevel level)
{
_logFilePath = logFilePath;
this.SetRecordLevel(level);
}
public override void Begin(TraceRecordLevel level, string category, string message)
{
if (!ShouldLog(level)) return;
lock (_lock)
{
var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {category}: {message}";
File.AppendAllText(_logFilePath, logEntry + Environment.NewLine);
}
}
protected override void End(TraceRecordLevel level, string category, string message)
{
// Optional: Log completion messages
}
private bool ShouldLog(TraceRecordLevel level)
{
return level <= this.RecordLevel;
}
}
// Usage
var doc = Document.ParseDocument("template.html");
doc.TraceLog = new FileTraceLog("pdf-generation.log", TraceRecordLevel.Messages);
doc.SaveAsPDF(stream);
Database Logging
using Scryber.Logging;
using System;
public class DatabaseTraceLog : PDFTraceLog
{
private readonly string _connectionString;
private readonly string _sessionId;
public DatabaseTraceLog(string connectionString, TraceRecordLevel level)
{
_connectionString = connectionString;
_sessionId = Guid.NewGuid().ToString();
this.SetRecordLevel(level);
}
public override void Begin(TraceRecordLevel level, string category, string message)
{
if (!ShouldLog(level)) return;
try
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
var command = new SqlCommand(
"INSERT INTO PDFLogs (SessionId, Timestamp, Level, Category, Message) " +
"VALUES (@SessionId, @Timestamp, @Level, @Category, @Message)",
connection);
command.Parameters.AddWithValue("@SessionId", _sessionId);
command.Parameters.AddWithValue("@Timestamp", DateTime.Now);
command.Parameters.AddWithValue("@Level", level.ToString());
command.Parameters.AddWithValue("@Category", category);
command.Parameters.AddWithValue("@Message", message);
command.ExecuteNonQuery();
}
}
catch (Exception ex)
{
// Don't let logging errors break PDF generation
Console.WriteLine($"Logging error: {ex.Message}");
}
}
private bool ShouldLog(TraceRecordLevel level)
{
return level <= this.RecordLevel;
}
}
// Usage
var doc = Document.ParseDocument("template.html");
doc.TraceLog = new DatabaseTraceLog("connection-string", TraceRecordLevel.Warnings);
doc.SaveAsPDF(stream);
Structured Logging (Serilog, NLog)
using Scryber.Logging;
using Serilog;
public class SerilogTraceLog : PDFTraceLog
{
private readonly ILogger _logger;
public SerilogTraceLog(ILogger logger, TraceRecordLevel level)
{
_logger = logger;
this.SetRecordLevel(level);
}
public override void Begin(TraceRecordLevel level, string category, string message)
{
if (!ShouldLog(level)) return;
switch (level)
{
case TraceRecordLevel.Errors:
_logger.Error("[{Category}] {Message}", category, message);
break;
case TraceRecordLevel.Warnings:
_logger.Warning("[{Category}] {Message}", category, message);
break;
case TraceRecordLevel.Messages:
_logger.Information("[{Category}] {Message}", category, message);
break;
case TraceRecordLevel.Verbose:
_logger.Debug("[{Category}] {Message}", category, message);
break;
}
}
private bool ShouldLog(TraceRecordLevel level)
{
return level <= this.RecordLevel;
}
}
// Usage with Serilog
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/pdf-generation.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
var doc = Document.ParseDocument("template.html");
doc.TraceLog = new SerilogTraceLog(Log.Logger, TraceRecordLevel.Messages);
doc.SaveAsPDF(stream);
Practical Examples
Example 1: Production Invoice Generator with Logging
using Scryber.Components;
using Scryber.Logging;
using System;
using System.IO;
using System.Linq;
public class InvoiceGenerator
{
private readonly string _logDirectory;
public InvoiceGenerator(string logDirectory)
{
_logDirectory = logDirectory;
}
public void GenerateInvoice(Invoice invoice, Stream output)
{
// Set up logging for this invoice
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
try
{
// Load and configure document
var doc = Document.ParseDocument("invoice-template.html");
doc.TraceLog = logger;
// Set metadata
doc.Info.Title = $"Invoice {invoice.Number}";
doc.Info.Author = "Acme Corp";
// Bind data
doc.Params["invoice"] = invoice;
// Generate
doc.SaveAsPDF(output);
// Check for warnings
var warnings = logger.Entries
.Where(e => e.Level == TraceRecordLevel.Warnings)
.ToList();
if (warnings.Any())
{
LogWarningsToFile(invoice.Number, warnings);
}
Console.WriteLine($"Invoice {invoice.Number} generated successfully");
}
catch (Exception ex)
{
// Log error with all diagnostic information
LogErrorToFile(invoice.Number, ex, logger.Entries);
throw;
}
}
private void LogWarningsToFile(string invoiceNumber, List<TraceRecord> warnings)
{
var logFile = Path.Combine(_logDirectory, $"warnings_{DateTime.Now:yyyyMMdd}.log");
var logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Invoice {invoiceNumber} - " +
$"{warnings.Count} warnings:{Environment.NewLine}";
foreach (var warning in warnings)
{
logEntry += $" - {warning.Category}: {warning.Message}{Environment.NewLine}";
}
File.AppendAllText(logFile, logEntry);
}
private void LogErrorToFile(string invoiceNumber, Exception ex, TraceRecordCollection entries)
{
var logFile = Path.Combine(_logDirectory, $"errors_{DateTime.Now:yyyyMMdd}.log");
var logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Invoice {invoiceNumber} FAILED{Environment.NewLine}" +
$"Error: {ex.Message}{Environment.NewLine}" +
$"Stack: {ex.StackTrace}{Environment.NewLine}" +
$"Log entries:{Environment.NewLine}";
foreach (var entry in entries)
{
logEntry += $" [{entry.Level}] {entry.Category}: {entry.Message}{Environment.NewLine}";
}
File.AppendAllText(logFile, logEntry);
}
}
// Usage
var generator = new InvoiceGenerator("./logs");
using (var output = new FileStream("invoice.pdf", FileMode.Create))
{
generator.GenerateInvoice(invoice, output);
}
Example 2: Performance Monitoring
using Scryber.Components;
using Scryber.Logging;
using System;
using System.Diagnostics;
using System.IO;
public class PerformanceMonitor
{
public void GenerateWithPerformanceLogging(string templatePath, Stream output)
{
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Verbose);
var stopwatch = Stopwatch.StartNew();
var doc = Document.ParseDocument(templatePath);
doc.TraceLog = logger;
var parseTime = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Parse time: {parseTime}ms");
stopwatch.Restart();
doc.SaveAsPDF(output);
var renderTime = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Render time: {renderTime}ms");
Console.WriteLine($"Total time: {parseTime + renderTime}ms");
// Analyze log for performance bottlenecks
AnalyzePerformance(logger.Entries);
}
private void AnalyzePerformance(TraceRecordCollection entries)
{
Console.WriteLine("\nPerformance Analysis:");
// Count operations by category
var categories = entries.GroupBy(e => e.Category)
.Select(g => new { Category = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count);
foreach (var cat in categories)
{
Console.WriteLine($" {cat.Category}: {cat.Count} operations");
}
// Find slow operations (if logged with timing)
var slowOps = entries
.Where(e => e.Message.Contains("ms") || e.Message.Contains("slow"))
.ToList();
if (slowOps.Any())
{
Console.WriteLine("\nPotential bottlenecks:");
foreach (var op in slowOps)
{
Console.WriteLine($" - {op.Message}");
}
}
}
}
// Usage
var monitor = new PerformanceMonitor();
using (var output = new FileStream("output.pdf", FileMode.Create))
{
monitor.GenerateWithPerformanceLogging("template.html", output);
}
Log Analysis and Troubleshooting
Common Error Patterns
public class LogAnalyzer
{
public void AnalyzeLogs(TraceRecordCollection entries)
{
// Check for errors
var errors = entries.Where(e => e.Level == TraceRecordLevel.Errors).ToList();
if (errors.Any())
{
Console.WriteLine($"ERRORS FOUND: {errors.Count}");
foreach (var error in errors)
{
Console.WriteLine($" - {error.Category}: {error.Message}");
}
}
// Check for warnings
var warnings = entries.Where(e => e.Level == TraceRecordLevel.Warnings).ToList();
if (warnings.Any())
{
Console.WriteLine($"\nWARNINGS: {warnings.Count}");
foreach (var warning in warnings)
{
Console.WriteLine($" - {warning.Category}: {warning.Message}");
}
}
// Check for common issues
CheckForCommonIssues(entries);
}
private void CheckForCommonIssues(TraceRecordCollection entries)
{
// Missing resources
if (entries.Any(e => e.Message.Contains("not found") || e.Message.Contains("missing")))
{
Console.WriteLine("\n⚠ Possible missing resources detected");
}
// Font issues
if (entries.Any(e => e.Category.Contains("Font") && e.Level == TraceRecordLevel.Warnings))
{
Console.WriteLine("\n⚠ Font loading issues detected");
}
// Image issues
if (entries.Any(e => e.Category.Contains("Image") && e.Level == TraceRecordLevel.Warnings))
{
Console.WriteLine("\n⚠ Image loading issues detected");
}
// Performance concerns
var verboseCount = entries.Count(e => e.Level == TraceRecordLevel.Verbose);
if (verboseCount > 10000)
{
Console.WriteLine($"\n⚠ High operation count ({verboseCount}) - potential performance issue");
}
}
}
Try It Yourself
Exercise 1: Custom Logger
Implement a custom logger that:
- Writes to a file
- Rotates log files daily
- Includes timestamps
- Filters by level
- Tests with document generation
Exercise 2: Log Dashboard
Create a simple dashboard that:
- Collects logs from multiple generations
- Shows error/warning counts
- Displays recent entries
- Highlights critical issues
Exercise 3: Performance Profiler
Build a profiler that:
- Measures generation time
- Tracks operations by category
- Identifies slowest operations
- Generates performance report
Common Pitfalls
❌ No Logging in Production
// Can't diagnose issues
doc.TraceLog = null;
doc.SaveAsPDF(stream);
✅ Solution:
// Always log at least warnings
doc.TraceLog = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
doc.SaveAsPDF(stream);
❌ Too Much Logging in Production
// Excessive log volume, performance impact
doc.TraceLog = new PDFCollectorTraceLog(TraceRecordLevel.Verbose);
✅ Solution:
// Appropriate level for production
doc.TraceLog = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
❌ Ignoring Log Output
// Logs generated but never reviewed
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
doc.TraceLog = logger;
doc.SaveAsPDF(stream);
// Log entries never checked
✅ Solution:
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
doc.TraceLog = logger;
doc.SaveAsPDF(stream);
// Check for issues
if (logger.Entries.Any(e => e.Level == TraceRecordLevel.Warnings))
{
AnalyzeAndReport(logger.Entries);
}
Logging Best Practices Checklist
- Logging enabled in development (Verbose)
- Logging enabled in production (Warnings or Messages)
- Log output reviewed regularly
- Errors logged with context
- Performance logged for optimization
- Log level appropriate for environment
- Custom logger implemented if needed
- Log rotation configured for file logging
Best Practices
- Always Log - Even if only errors
- Verbose in Development - See everything during testing
- Warnings in Production - Balance detail and performance
- Review Logs Regularly - Don’t just collect
- Log Context - Include document identifiers
- Handle Logging Errors - Don’t break generation
- Rotate Log Files - Prevent disk space issues
- Structured Logging - Use existing frameworks when possible
Next Steps
- Error Handling & Conformance - Handle errors gracefully
- Security - Secure your documents
- Optimization & Performance - Optimize generation
Continue learning → Error Handling & Conformance