Understanding the Problem
Due to deserialization errors, the default LogViewer in Umbraco may fail to parse JSON-formatted log files.
This issue is well-documented, as seen in our blog post Fixing the Reader's MaxDepth of 64 Has Been Exceeded in Umbraco.
We submitted a pull request to the Umbraco core repository to address this.
During discussions, Bjarke Berg suggested an alternative: replacing the default LogViewer with a custom implementation.
Here, we’ll show you how to implement this solution.
Steps to Replace LogViewer in Umbraco v13
To replace the default LogViewer, you’ll need to:
-
Create a Composer
-
Implement a Custom LogViewer
-
Register the Custom LogViewer
Let’s break down each step in detail:
Step 1: Creating a Composer
A composer in Umbraco allows you to configure the application during startup.
The CustomLogViewerComposer class is a simple implementation of IComposer, which adds your custom LogViewer to the builder:
public class CustomLogViewerComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.SetCustomLogViewer();
}
}
This composer ensures that your custom LogViewer replaces the default implementation during application startup.
Step 2: Implementing a Custom LogViewer
The custom LogViewer class extends SerilogLogViewerSourceBase and provides a more robust implementation.
Here’s a snippet of the UmbracoCustomLogViewer:
internal class UmbracoCustomLogViewer : SerilogLogViewerSourceBase
{
private const int FileSizeCap = 100;
private readonly ILogger<UmbracoCustomLogViewer> _logger;
private readonly string _logsPath;
public UmbracoCustomLogViewer(
ILogger<UmbracoCustomLogViewer> logger,
ILogViewerConfig logViewerConfig,
ILoggingConfiguration loggingConfiguration,
ILogLevelLoader logLevelLoader,
ILogger serilogLog)
: base(logViewerConfig, logLevelLoader, serilogLog)
{
_logger = logger;
_logsPath = loggingConfiguration.LogDirectory;
}
public override bool CanHandleLargeLogs => false;
public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod)
{
// Log Directory
var logDirectory = _logsPath;
// Number of entries
long fileSizeCount = 0;
// foreach full day in the range - see if we can find one or more filenames that end with
// yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1))
{
// Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);
fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length);
}
// The GetLogSize call on JsonLogViewer returns the total file size in bytes
// Check if the log size is not greater than 100Mb (FileSizeCap)
var logSizeAsMegabytes = fileSizeCount / 1024 / 1024;
return logSizeAsMegabytes <= FileSizeCap;
}
protected override IReadOnlyList<LogEvent> GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip,
int take)
{
var logs = new List<LogEvent>();
var count = 0;
var serializerSettings = GetJsonSerializerSettings();
var jsonSerializer = JsonSerializer.Create(serializerSettings);
// foreach full day in the range - see if we can find one or more filenames that end with
// yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1))
{
// Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind);
// Foreach file we find - open it
foreach (var filePath in filesForCurrentDay)
{
// Open log file & add contents to the log collection
// Which we then use LINQ to page over
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var stream = new StreamReader(fs))
{
var reader = new LogEventReader(stream, jsonSerializer);
while (TryRead(reader, filePath, out LogEvent? evt))
{
// We may get a null if log line is malformed
if (evt == null)
{
continue;
}
if (count > skip + take)
{
break;
}
if (count < skip)
{
count++;
continue;
}
if (filter.TakeLogEvent(evt))
{
logs.Add(evt);
}
count++;
}
}
}
}
}
return logs;
}
private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
private bool TryRead(LogEventReader reader, string filePath, out LogEvent? evt)
{
evt = null;
try
{
return reader.TryRead(out evt);
}
catch (JsonReaderException ex)
{
_logger.LogError(ex, "JSON Reader error: Unable to parse log event in file '{FilePath}'.", filePath);
return false;
}
catch (JsonSerializationException ex)
{
_logger.LogError(ex, "JSON Serialization error: Unable to deserialize log event in file '{FilePath}'.", filePath);
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred while reading a log event from file '{FilePath}'.", filePath);
throw;
}
}
private JsonSerializerSettings GetJsonSerializerSettings()
{
return new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Culture = System.Globalization.CultureInfo.InvariantCulture,
MaxDepth = 128,
MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
Error = (sender, args) =>
{
Console.Error.WriteLine($"Error during deserialization: {args.ErrorContext.Error.Message}");
args.ErrorContext.Handled = true;
}
};
}
}
Key features include:
-
Custom JSON Serializer: Adjusts MaxDepth and handles missing or null values gracefully.
-
Error Handling: Implements detailed logging for JSON parsing errors to avoid crashes.
Optimizing the UmbracoCustomLogViewer class is beyond the scope of this article. We decided to keep the original Umbraco code for better readability and future investigative purposes. Note the correct error handling.
Step 3: Registering the Custom LogViewer
Finally, register your custom LogViewer using an extension method on IUmbracoBuilder:
/// <summary>
/// Replaces the existing log viewer implementation with a custom log viewer.
/// This method searches for an existing `ILogViewer` service registration, removes it if it derives from
/// `SerilogLogViewerSourceBase`, and registers a custom implementation of `ILogViewer`.
/// </summary>
/// <param name="builder">The Umbraco builder to configure.</param>
/// <returns>The modified Umbraco builder with the custom log viewer configured.</returns>
public static IUmbracoBuilder SetCustomLogViewer(this IUmbracoBuilder builder)
{
/* Remove default LogViewer (optional) */
// var existingLogViewerRegistration = builder.Services.FirstOrDefault(service =>
// service.ServiceType == typeof(ILogViewer) &&
// typeof(SerilogLogViewerSourceBase).IsAssignableFrom(service.ImplementationType));
//
// if (existingLogViewerRegistration != null)
// builder.Services.Remove(existingLogViewerRegistration);
builder.Services.AddSingleton<ILogViewer, UmbracoCustomLogViewer>();
/* Registering new LogViewer in the following way as in Umbraco.Core is not needed */
// builder.SetLogViewer<UmbracoCustomLogViewer>();
// builder.Services.AddSingleton<ILogViewer>(factory => new UmbracoCustomLogViewer(
// factory.GetRequiredService<ILogger<UmbracoCustomLogViewer>>(),
// factory.GetRequiredService<ILogViewerConfig>(),
// factory.GetRequiredService<ILoggingConfiguration>(),
// factory.GetRequiredService<ILogLevelLoader>(),
// Log.Logger));
return builder;
}
This method ensures that your custom LogViewer replaces the default implementation.
Step 4: Testing the LogViewer Implementation
After implementing and registering the custom LogViewer, verifying its functionality is essential.
Navigate to the LogViewer in the Umbraco back office:
1. Go to /umbraco#/settings/logViewer/overview.
2. Perform smoke tests to ensure proper functionality:
- Display all error logs.
- Navigate through multiple pages of logs to review older entries.
- Confirm that no errors occur while loading logs.
These steps will validate that the custom LogViewer handles logs correctly and resolves the issues encountered with the default implementation.
Conclusion
Replacing the default LogViewer with a custom implementation in Umbraco v13 is a practical solution to overcome its limitations.
If you found this article helpful, check our blog for insights and solutions for Umbraco development challenges.