Jump to Section
What Causes the Error?
The root of the problem lies in a breaking change introduced in Newtonsoft.Json v13.0.1, which sets a default maximum depth for JSON deserialization:
internal const int DefaultMaxDepth = 64;
This limitation impacts Umbraco’s LogViewer when processing deeply nested JSON, triggering the following exception:
Newtonsoft.Json.JsonReaderException: The reader's MaxDepth of 64 has been exceeded. Path '@x.@x...', line 1, position 7441.
You can compare all Newtonsoft.Json releases here:
As you can see, version Newtonsoft.Json 13.0.1 introduces a breaking change that affects the Umbraco log viewer panel when getting logs.
Unable to parse a line in the JSON log file
Newtonsoft.Json.JsonReaderException: The reader's MaxDepth of 64 has been exceeded. Path '@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x', line 1, position 7441.
at Newtonsoft.Json.JsonReader.Push(JsonContainerType value)
at Newtonsoft.Json.JsonTextReader.ParseValue()
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateJObject(JsonReader reader)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
at Serilog.Formatting.Compact.Reader.LogEventReader.TryRead(LogEvent& evt)
at Umbraco.Cms.Core.Logging.Viewer.SerilogJsonLogViewer.TryRead(LogEventReader reader, LogEvent& evt)
The Fix: Modify MaxDepth in JsonSerializerSettings
The solution involves adjusting the maximum depth for JSON deserialization in the Umbraco project.
Here’s how you can fix this issue.
Step 1: Update the Log Viewer Code
In the SerilogJsonLogViewer class, modify the JSON serializer settings to allow a higher MaxDepth value.
For example:
private JsonSerializerSettings GetJsonSerializerSettings()
{
return new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Culture = System.Globalization.CultureInfo.InvariantCulture,
MaxDepth = 128 // Increased limit to handle deeply nested JSON
};
}
This change increases the maximum depth to 128, ensuring that your log files are processed correctly.
Step 2: Improved Error Handling
To make logging system more robust, implement enhanced error handling in the TryRead method.
Here’s an example:
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 in file '{filePath}': {ex.Message}");
return false;
}
catch (JsonSerializationException ex)
{
_logger.LogError(ex, $"JSON Serialization error in file '{filePath}': {ex.Message}");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error in file '{filePath}': {ex.Message}");
throw;
}
}
Wrapping up SerilogJsonLogViewer.cs
Here is the full SerilogJsonLogViewer class with better error handling and increased MaxDepth when reading JSON:
namespace Umbraco.Cms.Core.Logging.Viewer;
internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase
{
private const int FileSizeCap = 100;
private readonly ILogger<SerilogJsonLogViewer> _logger;
private readonly string _logsPath;
public SerilogJsonLogViewer(
ILogger<SerilogJsonLogViewer> 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}'.");
return false;
}
catch (JsonSerializationException ex)
{
_logger.LogError(ex, $"JSON Serialization error: Unable to deserialize log event in file '{filePath}'.");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, $"An unexpected error occurred while reading a log event from file '{filePath}'.");
throw;
}
}
private JsonSerializerSettings GetJsonSerializerSettings()
{
return new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Culture = System.Globalization.CultureInfo.InvariantCulture,
MaxDepth = 128
};
}
}
Key Insights on Serialization and Error Handling
Serialization
- GetJsonSerializerSettings() provides control over JSON parsing.
- DateParseHandling.None: Prevents automatic date conversion, preserving raw date formats.
- MaxDepth = 128: This limit on deserialization depth prevents potential stack overflow or excessive resource consumption from deeply nested JSON.
- JsonSerializer.Create(serializerSettings) creates a reusable serializer instance, avoiding repeated initialization overhead during log processing.
Error Handling
- Catches specific JSON-related exceptions (JsonReaderException and JsonSerializationException) to log meaningful error messages based on the nature of the issue (e.g., malformed JSON or deserialization failures).
- Logs the exact exception type and message for better diagnostic insights.
- All log messages include critical context, like the file path, to quickly identify the source of the issue.
Learn More
Current Umbraco 13.5.2 SerilogJsonLogViewer.cs implementation https://github.com/umbraco/Umbraco-CMS/blob/release-13.5.2/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs
The issue was raised on GitHub: https://github.com/umbraco/Umbraco-CMS/issues/17629
Pull request with the fix: https://github.com/umbraco/Umbraco-CMS/pull/17630
What's Next?
Explore our blog for more insights, and feel free to reach out for any queries or discussions related to Umbraco development.