Skip to content

Make logging more friendly for docker #19818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Umbraco.Core/Services/ILogViewerRepository.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Logging.Viewer;

namespace Umbraco.Cms.Core.Services;

/// <summary>
/// Represents a repository for viewing logs in Umbraco.
/// </summary>
public interface ILogViewerRepository
{
/// <summary>
Expand Down
237 changes: 18 additions & 219 deletions src/Umbraco.Core/Services/LogViewerService.cs
Original file line number Diff line number Diff line change
@@ -1,212 +1,58 @@
using System.Collections.ObjectModel;
using Umbraco.Cms.Core.Logging;

Check notice on line 1 in src/Umbraco.Core/Services/LogViewerService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Code Duplication

The module no longer contains too many functions with similar structure

Check notice on line 1 in src/Umbraco.Core/Services/LogViewerService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Primitive Obsession

The ratio of primivite types in function arguments is no longer above the threshold
using Umbraco.Cms.Core.Logging.Viewer;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
using LogLevel = Umbraco.Cms.Core.Logging.LogLevel;

namespace Umbraco.Cms.Core.Services;

public class LogViewerService : ILogViewerService
/// <summary>
/// Represents a service for viewing logs in Umbraco.
/// </summary>
public class LogViewerService : LogViewerServiceBase
{
private const int FileSizeCap = 100;
private readonly ILogViewerQueryRepository _logViewerQueryRepository;
private readonly ICoreScopeProvider _provider;
private readonly ILoggingConfiguration _loggingConfiguration;
private readonly ILogViewerRepository _logViewerRepository;

/// <summary>
/// Initializes a new instance of the <see cref="LogViewerService"/> class.
/// </summary>
public LogViewerService(
ILogViewerQueryRepository logViewerQueryRepository,
ICoreScopeProvider provider,
ILoggingConfiguration loggingConfiguration,
ILogViewerRepository logViewerRepository)
: base(
logViewerQueryRepository,
provider,
logViewerRepository)
{
_logViewerQueryRepository = logViewerQueryRepository;
_provider = provider;
_loggingConfiguration = loggingConfiguration;

Check notice on line 30 in src/Umbraco.Core/Services/LogViewerService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Excess Number of Function Arguments

GetPagedLogsAsync is no longer above the threshold for number of arguments
_logViewerRepository = logViewerRepository;
}

/// <inheritdoc/>
public Task<Attempt<PagedModel<ILogEntry>?, LogViewerOperationStatus>> GetPagedLogsAsync(
DateTimeOffset? startDate,
DateTimeOffset? endDate,
int skip,
int take,
Direction orderDirection = Direction.Descending,
string? filterExpression = null,
string[]? logLevels = null)
{
LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate);

// We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs(logTimePeriod) == false)
{
return Task.FromResult(Attempt.FailWithStatus<PagedModel<ILogEntry>?, LogViewerOperationStatus>(
LogViewerOperationStatus.CancelledByLogsSizeValidation,
null));
}


PagedModel<ILogEntry> filteredLogs = GetFilteredLogs(logTimePeriod, filterExpression, logLevels, orderDirection, skip, take);

return Task.FromResult(Attempt.SucceedWithStatus<PagedModel<ILogEntry>?, LogViewerOperationStatus>(
LogViewerOperationStatus.Success,
filteredLogs));
}

/// <inheritdoc/>
public Task<PagedModel<ILogViewerQuery>> GetSavedLogQueriesAsync(int skip, int take)
{
using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true);
ILogViewerQuery[] savedLogQueries = _logViewerQueryRepository.GetMany().ToArray();
var pagedModel = new PagedModel<ILogViewerQuery>(savedLogQueries.Length, savedLogQueries.Skip(skip).Take(take));
return Task.FromResult(pagedModel);
}
protected override string LoggerName => "UmbracoFile";

/// <inheritdoc/>
public Task<ILogViewerQuery?> GetSavedLogQueryByNameAsync(string name)
public override Task<Attempt<bool, LogViewerOperationStatus>> CanViewLogsAsync(LogTimePeriod logTimePeriod)
{
using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true);
return Task.FromResult(_logViewerQueryRepository.GetByName(name));
}

/// <inheritdoc/>
public async Task<Attempt<ILogViewerQuery?, LogViewerOperationStatus>> AddSavedLogQueryAsync(string name, string query)
{
ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name);

if (logViewerQuery is not null)
{
return Attempt.FailWithStatus<ILogViewerQuery?, LogViewerOperationStatus>(LogViewerOperationStatus.DuplicateLogSearch, null);
}

logViewerQuery = new LogViewerQuery(name, query);

using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true);
_logViewerQueryRepository.Save(logViewerQuery);

return Attempt.SucceedWithStatus<ILogViewerQuery?, LogViewerOperationStatus>(LogViewerOperationStatus.Success, logViewerQuery);
}

/// <inheritdoc/>
public async Task<Attempt<ILogViewerQuery?, LogViewerOperationStatus>> DeleteSavedLogQueryAsync(string name)
{
ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name);

if (logViewerQuery is null)
{
return Attempt.FailWithStatus<ILogViewerQuery?, LogViewerOperationStatus>(LogViewerOperationStatus.NotFoundLogSearch, null);
}

using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true);
_logViewerQueryRepository.Delete(logViewerQuery);

return Attempt.SucceedWithStatus<ILogViewerQuery?, LogViewerOperationStatus>(LogViewerOperationStatus.Success, logViewerQuery);
}

/// <inheritdoc/>
public Task<Attempt<bool, LogViewerOperationStatus>> CanViewLogsAsync(DateTimeOffset? startDate, DateTimeOffset? endDate)
{
LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate);
bool isAllowed = CanViewLogs(logTimePeriod);

if (isAllowed == false)
{
return Task.FromResult(Attempt.FailWithStatus(LogViewerOperationStatus.CancelledByLogsSizeValidation, isAllowed));
}

return Task.FromResult(Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, isAllowed));
}

/// <inheritdoc/>
public Task<Attempt<LogLevelCounts?, LogViewerOperationStatus>> GetLogLevelCountsAsync(DateTimeOffset? startDate, DateTimeOffset? endDate)
{
LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate);

// We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs(logTimePeriod) == false)
{
return Task.FromResult(Attempt.FailWithStatus<LogLevelCounts?, LogViewerOperationStatus>(
return Task.FromResult(Attempt.FailWithStatus(
LogViewerOperationStatus.CancelledByLogsSizeValidation,
null));
}

LogLevelCounts counter = _logViewerRepository.GetLogCount(logTimePeriod);

return Task.FromResult(Attempt.SucceedWithStatus<LogLevelCounts?, LogViewerOperationStatus>(
LogViewerOperationStatus.Success,
counter));
}

/// <inheritdoc/>
public Task<Attempt<PagedModel<LogTemplate>, LogViewerOperationStatus>> GetMessageTemplatesAsync(DateTimeOffset? startDate, DateTimeOffset? endDate, int skip, int take)
{
LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate);

// We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs(logTimePeriod) == false)
{
return Task.FromResult(Attempt.FailWithStatus<PagedModel<LogTemplate>, LogViewerOperationStatus>(
LogViewerOperationStatus.CancelledByLogsSizeValidation,
null!));
}

LogTemplate[] messageTemplates = _logViewerRepository.GetMessageTemplates(logTimePeriod);

return Task.FromResult(Attempt.SucceedWithStatus(
LogViewerOperationStatus.Success,
new PagedModel<LogTemplate>(messageTemplates.Length, messageTemplates.Skip(skip).Take(take))));
}

/// <inheritdoc/>
public ReadOnlyDictionary<string, LogLevel> GetLogLevelsFromSinks()
{
var configuredLogLevels = new Dictionary<string, LogLevel>
{
{ "Global", GetGlobalMinLogLevel() },
{ "UmbracoFile", _logViewerRepository.RestrictedToMinimumLevel() },
};

return configuredLogLevels.AsReadOnly();
}

/// <inheritdoc/>
public LogLevel GetGlobalMinLogLevel() => _logViewerRepository.GetGlobalMinLogLevel();

/// <summary>
/// Returns a <see cref="LogTimePeriod" /> representation from a start and end date for filtering log files.
/// </summary>
/// <param name="startDate">The start date for the date range (can be null).</param>
/// <param name="endDate">The end date for the date range (can be null).</param>
/// <returns>The LogTimePeriod object used to filter logs.</returns>
private LogTimePeriod GetTimePeriod(DateTimeOffset? startDate, DateTimeOffset? endDate)
{
if (startDate is null || endDate is null)
{
DateTime now = DateTime.Now;
if (startDate is null)
{
startDate = now.AddDays(-1);
}

if (endDate is null)
{
endDate = now;
}
isAllowed));
}

return new LogTimePeriod(startDate.Value.LocalDateTime, endDate.Value.LocalDateTime);
return Task.FromResult(Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, isAllowed));
}

/// <summary>
/// Returns a value indicating whether to stop a GET request that is attempting to fetch logs from a 1GB file.
/// </summary>
/// <param name="logTimePeriod">The time period to filter the logs.</param>
/// <returns>The value whether or not you are able to view the logs.</returns>
/// <returns>Whether you are able to view the logs.</returns>
private bool CanViewLogs(LogTimePeriod logTimePeriod)
{
// Number of entries
Expand All @@ -230,52 +76,5 @@
return logSizeAsMegabytes <= FileSizeCap;
}

private PagedModel<ILogEntry> GetFilteredLogs(
LogTimePeriod logTimePeriod,
string? filterExpression,
string[]? logLevels,
Direction orderDirection,
int skip,
int take)
{
IEnumerable<ILogEntry> logs = _logViewerRepository.GetLogs(logTimePeriod, filterExpression).ToArray();

// This is user used the checkbox UI to toggle which log levels they wish to see
// If an empty array or null - its implied all levels to be viewed
if (logLevels?.Length > 0)
{
var logsAfterLevelFilters = new List<ILogEntry>();
var validLogType = true;
foreach (var level in logLevels)
{
// Check if level string is part of the LogEventLevel enum
if (Enum.IsDefined(typeof(LogLevel), level))
{
validLogType = true;
logsAfterLevelFilters.AddRange(logs.Where(x =>
string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase)));
}
else
{
validLogType = false;
}
}

if (validLogType)
{
logs = logsAfterLevelFilters;
}
}

return new PagedModel<ILogEntry>
{
Total = logs.Count(),
Items = logs
.OrderBy(l => l.Timestamp, orderDirection)
.Skip(skip)
.Take(take),
};
}

private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
private static string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";

Check notice on line 79 in src/Umbraco.Core/Services/LogViewerService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Excess Number of Function Arguments

GetFilteredLogs is no longer above the threshold for number of arguments
}
Loading