Logging
See what happens as a document is generated - for debugging templates, monitoring production, and finding performance problems.
Learning Objectives
By the end of this article, you’ll be able to:
- Append the generation log to a document
- Choose the right log level
- Collect log entries in code
- Send the log to your own logging system
- Use the log to troubleshoot problems
Log Levels
| Level | Records |
|---|---|
Off |
Nothing |
Errors |
Errors only |
Warnings |
Errors and warnings |
Messages |
Errors, warnings and information messages |
Verbose |
Everything above, plus detailed processing steps |
Diagnostic |
Everything, including layout and positioning detail (engine logs only) |
Appending the Log to the Document
The quickest way to see what happened is to add the log as pages at the end of the PDF.
With the document builder
using Paperwork;
using Paperwork.Generation;
await factory.NewDocument()
.WithLayoutFile("invoice.html")
.WithData("model", invoice)
.WithLogLevel(PaperworkRequestLogOption.Messages)
.SaveAsync("invoice.pdf");
The builder levels are Template (the default - use the template’s own setting), Off, Errors, Warnings, Messages and Verbose.
In the template
<?paperwork append-log='true' log-level='Messages' ?>
<html xmlns='http://www.w3.org/1999/xhtml'>
...
</html>
On an engine Document
using var doc = Document.ParseDocument("invoice.html");
doc.AppendTraceLog = true;
doc.TraceLog.SetRecordLevel(TraceRecordLevel.Messages);
await factory.NewDocument()
.WithLayout(doc)
.SaveAsync("invoice.pdf");
The appended log is a development tool. Turn it off for documents that go to users.
Collecting Log Entries in Code
A CollectorTraceLog keeps the entries in memory, so they can be read after the document is generated. Add it to an engine Document (see The Engine Document).
using Scryber;
using Scryber.Components;
using Scryber.Logging;
using var doc = Document.ParseDocument("invoice.html");
var collector = new CollectorTraceLog(TraceRecordLevel.Warnings, "Invoice", autostart: true);
doc.AddTraceLog(collector);
await factory.NewDocument()
.WithLayout(doc)
.WithData("model", invoice)
.SaveAsync("invoice.pdf");
foreach (var entry in collector)
{
Console.WriteLine($"[{entry.Level}] {entry.Category}: {entry.Message}");
}
Log Level Selection
Development
Use Verbose, appended to the document, while building and testing templates.
builder.WithLogLevel(PaperworkRequestLogOption.Verbose);
Production
Collect Warnings and above in code, and send them to your logging system. Don’t append the log to the document.
var collector = new CollectorTraceLog(TraceRecordLevel.Warnings, "Production", autostart: true);
doc.AddTraceLog(collector);
Performance-Critical
Use Errors or Off - detailed logging takes time on large documents.
Custom Log Handlers
A custom log inherits from TraceLog and overrides Record. Every entry the document logs at or above its level is passed to it.
Forwarding to Microsoft.Extensions.Logging
using Microsoft.Extensions.Logging;
using Scryber;
using Scryber.Logging;
public class LoggerTraceLog : TraceLog
{
private readonly ILogger _logger;
public LoggerTraceLog(ILogger logger, TraceRecordLevel level)
: base(level, "Paperwork")
{
_logger = logger;
}
protected override void Record(string inset, TraceLevel level, TimeSpan timestamp,
string category, string message, Exception ex)
{
var logLevel = level switch
{
TraceLevel.Failure => LogLevel.Critical,
TraceLevel.Error => LogLevel.Error,
TraceLevel.Warning => LogLevel.Warning,
TraceLevel.Message => LogLevel.Information,
TraceLevel.Verbose => LogLevel.Debug,
_ => LogLevel.Trace
};
_logger.Log(logLevel, ex, "{Category}: {Message}", category, message);
}
}
Usage:
using var doc = Document.ParseDocument("invoice.html");
doc.AddTraceLog(new LoggerTraceLog(_logger, TraceRecordLevel.Warnings));
await factory.NewDocument()
.WithLayout(doc)
.SaveAsync("invoice.pdf");
The same pattern works for Serilog, NLog, a file or a database - write the entry in Record.
To add a log to every document without changing code, register a log factory in configuration. See Logging and Tracing.
Practical Example: Production Generator with Logging
public class InvoiceGenerator
{
private readonly IPaperworkFactory _factory;
private readonly ILogger<InvoiceGenerator> _logger;
public InvoiceGenerator(IPaperworkFactory factory, ILogger<InvoiceGenerator> logger)
{
_factory = factory;
_logger = logger;
}
public async Task<byte[]> GenerateAsync(Invoice invoice)
{
using var doc = Document.ParseDocument("Templates/invoice.html");
var collector = new CollectorTraceLog(TraceRecordLevel.Warnings, "Invoice", autostart: true);
doc.AddTraceLog(collector);
var result = await _factory.NewDocument()
.WithLayout(doc)
.WithData("model", invoice)
.BuildAsync();
// Report any warnings or errors with the invoice number
foreach (var entry in collector)
{
_logger.LogWarning("Invoice {Number} [{Level}] {Category}: {Message}",
invoice.Number, entry.Level, entry.Category, entry.Message);
}
if (!result.Success)
{
_logger.LogError("Invoice {Number} failed: {Message} {Error}",
invoice.Number, result.Message, result.Error);
throw new InvalidOperationException(result.Message);
}
return result.Document.Binary;
}
}
Log Analysis and Troubleshooting
Common Error Patterns
| Log message | Likely cause |
|---|---|
| Could not load the image for component … | The image path or url is wrong, or the request failed |
| The font … could not be loaded, falling back to Courier … | The font is not registered, or not at the requested weight or style |
| Cannot bind the values for … | An expression in the template failed - the message gives the reason |
| Skipping unknown attribute … | An unknown attribute in Lax mode - logged when parser-log='true' is set |
| No Type could be found in the namespace … | An unknown element in Lax mode - logged when parser-log='true' is set |
Invalid expression syntax, and unknown content in Strict mode, are not logged - they stop the document being generated. See Error Handling & Conformance.
Try It Yourself
Exercise 1: Custom Logger
Write a TraceLog that writes each entry to the console in colour by level.
Exercise 2: Template Health Check
Generate each of your templates with a CollectorTraceLog at Warnings, and list any template with entries.
Exercise 3: Find the Slow Template
Append the Verbose log to a large document, and look at the timings for each stage.
Common Pitfalls
❌ No Logging in Production
Problems in production documents go unnoticed.
âś… Solution: Collect Warnings and above, and send them to your logging system.
❌ Too Much Logging in Production
Verbose and Diagnostic logging slow down generation.
âś… Solution: Use Warnings or Errors in production.
❌ Appending the Log to User Documents
âś… Solution: Only use WithLogLevel or append-log while developing.
Logging Best Practices Checklist
Verboseappended log while developing templatesWarningscollected in production- Log entries sent to the application’s logging system
- Appended log turned off for user documents
- Template and data identifiers included in logged messages
Next Steps
- Error Handling & Conformance - Strict and Lax modes
- Security - Passwords and permissions
Continue learning → Error Handling & Conformance