Replace LogViewer with Custom Implementation in Umbraco v13

Umbraco’s built-in LogViewer is a helpful tool for monitoring and debugging your site’s behavior. However, it may encounter issues when handling log files in JSON format. One such issue, related to deserialization, can completely break the LogViewer interface in the back office. This article outlines how to replace the default LogViewer with a custom implementation to overcome these limitations and errors

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:

  1. Create a Composer

  2. Implement a Custom LogViewer

  3. 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. 

Navigating to Umbraco LogViewer in Backoffice

Navigating to Umbraco LogViewer in Backoffice

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.

↑ Top ↑