Make logging more friendly for docker (#19818)
* Cleanup obsoleted methods * Add a way to disable UmbracoFile default sink * Abstract LogViewService so only UmbracoFile sink related things are in the default interface implementation. * Abstract LogViewRepository so only UmbracoFile sink related things are in the default interface implementation. * Move GetGlobalLogLevelEventMinLevel to base * Removed unused internal class and obsoleted its base * Added missing XML header comments and resolved warnings in service and repository classes. * Made private method static. * Addressed issues raised in code review. * Expose repository from the service base class. * Restored further obsoleted code we can't remove yet. * Removed log viewer tests on removed class. We have integration tests for the new service. * Obsoleted ILogViewer interface. --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -1,212 +1,58 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
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;
|
||||
_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));
|
||||
}
|
||||
protected override string LoggerName => "UmbracoFile";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<PagedModel<ILogViewerQuery>> GetSavedLogQueriesAsync(int skip, int take)
|
||||
public override Task<Attempt<bool, LogViewerOperationStatus>> CanViewLogsAsync(LogTimePeriod logTimePeriod)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ILogViewerQuery?> GetSavedLogQueryByNameAsync(string name)
|
||||
{
|
||||
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.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>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return new LogTimePeriod(startDate.Value.LocalDateTime, endDate.Value.LocalDateTime);
|
||||
}
|
||||
|
||||
/// <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
|
||||
@@ -230,52 +76,5 @@ public class LogViewerService : ILogViewerService
|
||||
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";
|
||||
}
|
||||
|
||||
269
src/Umbraco.Core/Services/LogViewerServiceBase.cs
Normal file
269
src/Umbraco.Core/Services/LogViewerServiceBase.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using System.Collections.ObjectModel;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for log viewer services that provides common functionality for managing log entries and queries.
|
||||
/// </summary>
|
||||
public abstract class LogViewerServiceBase : ILogViewerService
|
||||
{
|
||||
private readonly ILogViewerQueryRepository _logViewerQueryRepository;
|
||||
private readonly ICoreScopeProvider _provider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LogViewerServiceBase"/> class.
|
||||
/// </summary>
|
||||
protected LogViewerServiceBase(
|
||||
ILogViewerQueryRepository logViewerQueryRepository,
|
||||
ICoreScopeProvider provider,
|
||||
ILogViewerRepository logViewerRepository)
|
||||
{
|
||||
_logViewerQueryRepository = logViewerQueryRepository;
|
||||
_provider = provider;
|
||||
LogViewerRepository = logViewerRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILogViewerRepository"/>.
|
||||
/// </summary>
|
||||
protected ILogViewerRepository LogViewerRepository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the logger.
|
||||
/// </summary>
|
||||
protected abstract string LoggerName { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ReadOnlyDictionary<string, LogLevel> GetLogLevelsFromSinks()
|
||||
{
|
||||
var configuredLogLevels = new Dictionary<string, LogLevel>
|
||||
{
|
||||
{ "Global", GetGlobalMinLogLevel() },
|
||||
{ LoggerName, LogViewerRepository.RestrictedToMinimumLevel() },
|
||||
};
|
||||
|
||||
return configuredLogLevels.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual LogLevel GetGlobalMinLogLevel() => LogViewerRepository.GetGlobalMinLogLevel();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task<ILogViewerQuery?> GetSavedLogQueryByNameAsync(string name)
|
||||
{
|
||||
using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true);
|
||||
return Task.FromResult(_logViewerQueryRepository.GetByName(name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual 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 virtual 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 virtual 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<Attempt<LogLevelCounts?, LogViewerOperationStatus>> GetLogLevelCountsAsync(
|
||||
DateTimeOffset? startDate, DateTimeOffset? endDate)
|
||||
{
|
||||
LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate);
|
||||
|
||||
Attempt<bool, LogViewerOperationStatus> canViewLogs = await CanViewLogsAsync(logTimePeriod);
|
||||
|
||||
// We will need to stop the request if trying to do this on a 1GB file
|
||||
if (canViewLogs.Success == false)
|
||||
{
|
||||
return Attempt.FailWithStatus<LogLevelCounts?, LogViewerOperationStatus>(
|
||||
LogViewerOperationStatus.CancelledByLogsSizeValidation,
|
||||
null);
|
||||
}
|
||||
|
||||
LogLevelCounts counter = LogViewerRepository.GetLogCount(logTimePeriod);
|
||||
|
||||
return Attempt.SucceedWithStatus<LogLevelCounts?, LogViewerOperationStatus>(
|
||||
LogViewerOperationStatus.Success,
|
||||
counter);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<Attempt<PagedModel<LogTemplate>, LogViewerOperationStatus>> GetMessageTemplatesAsync(
|
||||
DateTimeOffset? startDate, DateTimeOffset? endDate, int skip, int take)
|
||||
{
|
||||
LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate);
|
||||
|
||||
Attempt<bool, LogViewerOperationStatus> canViewLogs = await CanViewLogsAsync(logTimePeriod);
|
||||
|
||||
// We will need to stop the request if trying to do this on a 1GB file
|
||||
if (canViewLogs.Success == false)
|
||||
{
|
||||
return Attempt.FailWithStatus<PagedModel<LogTemplate>, LogViewerOperationStatus>(
|
||||
LogViewerOperationStatus.CancelledByLogsSizeValidation,
|
||||
null!);
|
||||
}
|
||||
|
||||
LogTemplate[] messageTemplates = LogViewerRepository.GetMessageTemplates(logTimePeriod);
|
||||
|
||||
return Attempt.SucceedWithStatus(
|
||||
LogViewerOperationStatus.Success,
|
||||
new PagedModel<LogTemplate>(messageTemplates.Length, messageTemplates.Skip(skip).Take(take)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async 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);
|
||||
|
||||
Attempt<bool, LogViewerOperationStatus> canViewLogs = await CanViewLogsAsync(logTimePeriod);
|
||||
|
||||
// We will need to stop the request if trying to do this on a 1GB file
|
||||
if (canViewLogs.Success == false)
|
||||
{
|
||||
return Attempt.FailWithStatus<PagedModel<ILogEntry>?, LogViewerOperationStatus>(
|
||||
LogViewerOperationStatus.CancelledByLogsSizeValidation,
|
||||
null);
|
||||
}
|
||||
|
||||
PagedModel<ILogEntry> filteredLogs =
|
||||
GetFilteredLogs(logTimePeriod, filterExpression, logLevels, orderDirection, skip, take);
|
||||
|
||||
return Attempt.SucceedWithStatus<PagedModel<ILogEntry>?, LogViewerOperationStatus>(
|
||||
LogViewerOperationStatus.Success,
|
||||
filteredLogs);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task<Attempt<bool, LogViewerOperationStatus>> CanViewLogsAsync(
|
||||
DateTimeOffset? startDate,
|
||||
DateTimeOffset? endDate)
|
||||
=> CanViewLogsAsync(GetTimePeriod(startDate, endDate));
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the logs for the specified time period can be viewed.
|
||||
/// </summary>
|
||||
public abstract Task<Attempt<bool, LogViewerOperationStatus>> CanViewLogsAsync(LogTimePeriod logTimePeriod);
|
||||
|
||||
|
||||
/// <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>
|
||||
protected virtual LogTimePeriod GetTimePeriod(DateTimeOffset? startDate, DateTimeOffset? endDate)
|
||||
{
|
||||
if (startDate is null || endDate is null)
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
startDate ??= now.AddDays(-1);
|
||||
endDate ??= now;
|
||||
}
|
||||
|
||||
return new LogTimePeriod(startDate.Value.LocalDateTime, endDate.Value.LocalDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a filtered page of logs from storage based on the provided parameters.
|
||||
/// </summary>
|
||||
protected 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the log viewer.</typeparam>
|
||||
/// <param name="builder">The builder.</param>
|
||||
[Obsolete("No longer used. Scheduled removal in Umbraco 18.")]
|
||||
public static IUmbracoBuilder SetLogViewer<T>(this IUmbracoBuilder builder)
|
||||
where T : class, ILogViewer
|
||||
{
|
||||
@@ -207,6 +208,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
/// <param name="factory">A function creating a log viewer.</param>
|
||||
[Obsolete("No longer used. Scheduled removal in Umbraco 18.")]
|
||||
public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func<IServiceProvider, ILogViewer> factory)
|
||||
{
|
||||
builder.Services.AddUnique(factory);
|
||||
@@ -218,6 +220,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// </summary>
|
||||
/// <param name="builder">A builder.</param>
|
||||
/// <param name="viewer">A log viewer.</param>
|
||||
[Obsolete("No longer used. Scheduled removal in Umbraco 18.")]
|
||||
public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer)
|
||||
{
|
||||
builder.Services.AddUnique(viewer);
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Umbraco.Extensions
|
||||
/// Such as adding ProcessID, Thread, AppDomain etc
|
||||
/// It is highly recommended that you keep/use this default in your own logging config customizations
|
||||
/// </summary>
|
||||
[Obsolete("Please use an alternative method. This will be removed in Umbraco 13.")]
|
||||
[Obsolete("Please use an alternative method. Scheduled for removal from Umbraco 13.")]
|
||||
public static LoggerConfiguration MinimalConfiguration(
|
||||
this LoggerConfiguration logConfig,
|
||||
Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
|
||||
@@ -37,7 +37,7 @@ namespace Umbraco.Extensions
|
||||
/// Such as adding ProcessID, Thread, AppDomain etc
|
||||
/// It is highly recommended that you keep/use this default in your own logging config customizations
|
||||
/// </summary>
|
||||
[Obsolete("Please use an alternative method. This will be removed in Umbraco 13.")]
|
||||
[Obsolete("Please use an alternative method. Scheduled for removal from Umbraco 13.")]
|
||||
public static LoggerConfiguration MinimalConfiguration(
|
||||
this LoggerConfiguration logConfig,
|
||||
Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
|
||||
@@ -68,7 +68,7 @@ namespace Umbraco.Extensions
|
||||
umbFileConfiguration = umbracoFileConfiguration;
|
||||
|
||||
logConfig.WriteTo.UmbracoFile(
|
||||
path : umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory),
|
||||
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory),
|
||||
fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
|
||||
restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
|
||||
rollingInterval: umbracoFileConfiguration.RollingInterval,
|
||||
@@ -79,7 +79,6 @@ namespace Umbraco.Extensions
|
||||
return logConfig;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This configures Serilog with some defaults
|
||||
/// Such as adding ProcessID, Thread, AppDomain etc
|
||||
@@ -108,14 +107,17 @@ namespace Umbraco.Extensions
|
||||
.Enrich.With<Log4NetLevelMapperEnricher>()
|
||||
.Enrich.FromLogContext(); // allows us to dynamically enrich
|
||||
|
||||
logConfig.WriteTo.UmbracoFile(
|
||||
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory, loggingConfiguration.LogFileNameFormat, loggingConfiguration.GetLogFileNameFormatArguments()),
|
||||
fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
|
||||
restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
|
||||
rollingInterval: umbracoFileConfiguration.RollingInterval,
|
||||
flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval,
|
||||
rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit,
|
||||
retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit);
|
||||
if (umbracoFileConfiguration.Enabled)
|
||||
{
|
||||
logConfig.WriteTo.UmbracoFile(
|
||||
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory, loggingConfiguration.LogFileNameFormat, loggingConfiguration.GetLogFileNameFormatArguments()),
|
||||
fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
|
||||
restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
|
||||
rollingInterval: umbracoFileConfiguration.RollingInterval,
|
||||
flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval,
|
||||
rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit,
|
||||
retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit);
|
||||
}
|
||||
|
||||
return logConfig;
|
||||
}
|
||||
@@ -127,7 +129,7 @@ namespace Umbraco.Extensions
|
||||
/// <param name="hostingEnvironment"></param>
|
||||
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
|
||||
///
|
||||
[Obsolete("Will be removed in Umbraco 13.")]
|
||||
[Obsolete("Scheduled for removal from Umbraco 13.")]
|
||||
public static LoggerConfiguration OutputDefaultTextFile(
|
||||
this LoggerConfiguration logConfig,
|
||||
Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
|
||||
@@ -136,7 +138,7 @@ namespace Umbraco.Extensions
|
||||
//Main .txt logfile - in similar format to older Log4Net output
|
||||
//Ends with ..txt as Date is inserted before file extension substring
|
||||
logConfig.WriteTo.File(
|
||||
Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"),
|
||||
Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"),
|
||||
shared: true,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
restrictedToMinimumLevel: minimumLevel,
|
||||
@@ -213,7 +215,6 @@ namespace Umbraco.Extensions
|
||||
null));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Outputs a CLEF format JSON log at /App_Data/Logs/
|
||||
/// </summary>
|
||||
@@ -222,7 +223,7 @@ namespace Umbraco.Extensions
|
||||
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
|
||||
/// <param name="hostingEnvironment"></param>
|
||||
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
|
||||
[Obsolete("Will be removed in Umbraco 13.")]
|
||||
[Obsolete("Scheduled for removal from Umbraco 13.")]
|
||||
public static LoggerConfiguration OutputDefaultJsonFile(
|
||||
this LoggerConfiguration logConfig,
|
||||
Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
|
||||
@@ -234,7 +235,7 @@ namespace Umbraco.Extensions
|
||||
// Ends with ..txt as Date is inserted before file extension substring
|
||||
logConfig.WriteTo.File(
|
||||
new CompactJsonFormatter(),
|
||||
Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles) ,$"UmbracoTraceLog.{Environment.MachineName}..json"),
|
||||
Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..json"),
|
||||
shared: true,
|
||||
rollingInterval: RollingInterval.Day, // Create a new JSON file every day
|
||||
retainedFileCountLimit: retainedFileCount, // Setting to null means we keep all files - default is 31 days
|
||||
@@ -270,6 +271,5 @@ namespace Umbraco.Extensions
|
||||
|
||||
return logConfig;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class SerilogLogger : IDisposable
|
||||
|
||||
public ILogger SerilogLog { get; }
|
||||
|
||||
[Obsolete]
|
||||
[Obsolete("Scheduled for removal in Umbraco 17.")]
|
||||
public static SerilogLogger CreateWithDefaultConfiguration(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
ILoggingConfiguration loggingConfiguration,
|
||||
@@ -32,7 +32,7 @@ public class SerilogLogger : IDisposable
|
||||
/// Creates a logger with some pre-defined configuration and remainder from config file
|
||||
/// </summary>
|
||||
/// <remarks>Used by UmbracoApplicationBase to get its logger.</remarks>
|
||||
[Obsolete]
|
||||
[Obsolete("Scheduled for removal in Umbraco 17.")]
|
||||
public static SerilogLogger CreateWithDefaultConfiguration(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
ILoggingConfiguration loggingConfiguration,
|
||||
|
||||
@@ -22,6 +22,7 @@ public class UmbracoFileConfiguration
|
||||
{
|
||||
IConfigurationSection? args = umbracoFileAppSettings.GetSection("Args");
|
||||
|
||||
Enabled = args.GetValue(nameof(Enabled), Enabled);
|
||||
RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel);
|
||||
FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes);
|
||||
RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval);
|
||||
@@ -31,6 +32,8 @@ public class UmbracoFileConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose;
|
||||
|
||||
public long FileSizeLimitBytes { get; set; } = 1073741824;
|
||||
|
||||
@@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Logging.Viewer;
|
||||
|
||||
[Obsolete("Use ILogViewerService instead. Scheduled removal in Umbraco 18.")]
|
||||
public interface ILogViewer
|
||||
{
|
||||
bool CanHandleLargeLogs { get; }
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Events;
|
||||
using Serilog.Formatting.Compact.Reader;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Umbraco.Cms.Core.Logging.Viewer;
|
||||
|
||||
internal sealed 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;
|
||||
|
||||
[Obsolete("Use ILogViewerService.CanViewLogsAsync instead. Scheduled for removal in Umbraco 15.")]
|
||||
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;
|
||||
|
||||
// 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);
|
||||
while (TryRead(reader, 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 static string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
|
||||
|
||||
private bool TryRead(LogEventReader reader, out LogEvent? evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TryRead(out evt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// As we are reading/streaming one line at a time in the JSON file
|
||||
// Thus we can not report the line number, as it will always be 1
|
||||
_logger.LogError(ex, "Unable to parse a line in the JSON log file");
|
||||
|
||||
evt = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Logging.Viewer;
|
||||
|
||||
[Obsolete("Use ILogViewerService instead. Scheduled removal in Umbraco 18.")]
|
||||
public abstract class SerilogLogViewerSourceBase : ILogViewer
|
||||
{
|
||||
private readonly ILogLevelLoader _logLevelLoader;
|
||||
|
||||
@@ -1,83 +1,40 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Events;
|
||||
using Serilog.Formatting.Compact.Reader;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Logging.Viewer;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Logging.Serilog;
|
||||
using LogLevel = Umbraco.Cms.Core.Logging.LogLevel;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
public class LogViewerRepository : ILogViewerRepository
|
||||
/// <summary>
|
||||
/// Repository for accessing log entries from the Umbraco log files stored on disk.
|
||||
/// </summary>
|
||||
public class LogViewerRepository : LogViewerRepositoryBase
|
||||
{
|
||||
private readonly ILoggingConfiguration _loggingConfiguration;
|
||||
private readonly ILogger<LogViewerRepository> _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly UmbracoFileConfiguration _umbracoFileConfig;
|
||||
|
||||
public LogViewerRepository(ILoggingConfiguration loggingConfiguration, ILogger<LogViewerRepository> logger, IJsonSerializer jsonSerializer, UmbracoFileConfiguration umbracoFileConfig)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LogViewerRepository"/> class.
|
||||
/// </summary>
|
||||
public LogViewerRepository(
|
||||
ILoggingConfiguration loggingConfiguration,
|
||||
ILogger<LogViewerRepository> logger,
|
||||
IJsonSerializer jsonSerializer,
|
||||
UmbracoFileConfiguration umbracoFileConfig)
|
||||
: base(umbracoFileConfig)
|
||||
{
|
||||
_loggingConfiguration = loggingConfiguration;
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_umbracoFileConfig = umbracoFileConfig;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ILogEntry> GetLogs(LogTimePeriod logTimePeriod, string? filterExpression = null)
|
||||
{
|
||||
var expressionFilter = new ExpressionFilter(filterExpression);
|
||||
|
||||
return GetLogs(logTimePeriod, expressionFilter);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogLevelCounts GetLogCount(LogTimePeriod logTimePeriod)
|
||||
{
|
||||
var counter = new CountingFilter();
|
||||
|
||||
GetLogs(logTimePeriod, counter);
|
||||
|
||||
return counter.Counts;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogTemplate[] GetMessageTemplates(LogTimePeriod logTimePeriod)
|
||||
{
|
||||
var messageTemplates = new MessageTemplateFilter();
|
||||
|
||||
GetLogs(logTimePeriod, messageTemplates);
|
||||
|
||||
return messageTemplates.Counts
|
||||
.Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value })
|
||||
.OrderByDescending(x => x.Count).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogLevel GetGlobalMinLogLevel()
|
||||
{
|
||||
LogEventLevel logLevel = GetGlobalLogLevelEventMinLevel();
|
||||
|
||||
return Enum.Parse<LogLevel>(logLevel.ToString());
|
||||
}
|
||||
|
||||
public LogLevel RestrictedToMinimumLevel()
|
||||
{
|
||||
LogEventLevel minLevel = _umbracoFileConfig.RestrictedToMinimumLevel;
|
||||
return Enum.Parse<LogLevel>(minLevel.ToString());
|
||||
}
|
||||
|
||||
private LogEventLevel GetGlobalLogLevelEventMinLevel() =>
|
||||
Enum.GetValues(typeof(LogEventLevel))
|
||||
.Cast<LogEventLevel>()
|
||||
.Where(Log.IsEnabled)
|
||||
.DefaultIfEmpty(LogEventLevel.Information)
|
||||
.Min();
|
||||
|
||||
private IEnumerable<ILogEntry> GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter)
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<ILogEntry> GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter)
|
||||
{
|
||||
var logs = new List<LogEvent>();
|
||||
|
||||
@@ -163,7 +120,7 @@ public class LogViewerRepository : ILogViewerRepository
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
|
||||
private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
|
||||
private static string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
|
||||
|
||||
private bool TryRead(LogEventReader reader, out LogEvent? evt)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Umbraco.Cms.Core.Logging.Viewer;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Logging.Serilog;
|
||||
using LogLevel = Umbraco.Cms.Core.Logging.LogLevel;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for log viewer repository implementations.
|
||||
/// </summary>
|
||||
public abstract class LogViewerRepositoryBase : ILogViewerRepository
|
||||
{
|
||||
private readonly UmbracoFileConfiguration _umbracoFileConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LogViewerRepositoryBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="umbracoFileConfig"></param>
|
||||
public LogViewerRepositoryBase(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<ILogEntry> GetLogs(LogTimePeriod logTimePeriod, string? filterExpression = null)
|
||||
{
|
||||
var expressionFilter = new ExpressionFilter(filterExpression);
|
||||
|
||||
return GetLogs(logTimePeriod, expressionFilter);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual LogLevelCounts GetLogCount(LogTimePeriod logTimePeriod)
|
||||
{
|
||||
var counter = new CountingFilter();
|
||||
|
||||
GetLogs(logTimePeriod, counter);
|
||||
|
||||
return counter.Counts;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual LogTemplate[] GetMessageTemplates(LogTimePeriod logTimePeriod)
|
||||
{
|
||||
var messageTemplates = new MessageTemplateFilter();
|
||||
|
||||
GetLogs(logTimePeriod, messageTemplates);
|
||||
|
||||
return messageTemplates.Counts
|
||||
.Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value })
|
||||
.OrderByDescending(x => x.Count).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual LogLevel GetGlobalMinLogLevel()
|
||||
{
|
||||
LogEventLevel logLevel = GetGlobalLogLevelEventMinLevel();
|
||||
|
||||
return Enum.Parse<LogLevel>(logLevel.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum-level log value from the config file.
|
||||
/// </summary>
|
||||
public virtual LogLevel RestrictedToMinimumLevel()
|
||||
{
|
||||
LogEventLevel minLevel = _umbracoFileConfig.RestrictedToMinimumLevel;
|
||||
return Enum.Parse<LogLevel>(minLevel.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum log level from the global Serilog configuration.
|
||||
/// </summary>
|
||||
protected virtual LogEventLevel GetGlobalLogLevelEventMinLevel() =>
|
||||
Enum.GetValues(typeof(LogEventLevel))
|
||||
.Cast<LogEventLevel>()
|
||||
.Where(Log.IsEnabled)
|
||||
.DefaultIfEmpty(LogEventLevel.Information)
|
||||
.Min();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the logs for a specified time period and filter.
|
||||
/// </summary>
|
||||
protected abstract IEnumerable<ILogEntry> GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter);
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Logging.Viewer;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging;
|
||||
|
||||
[TestFixture]
|
||||
public class LogviewerTests
|
||||
{
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var testRoot = TestContext.CurrentContext.TestDirectory.Split("bin")[0];
|
||||
|
||||
// Create an example JSON log file to check results
|
||||
// As a one time setup for all tets in this class/fixture
|
||||
var ioHelper = TestHelper.IOHelper;
|
||||
var hostingEnv = TestHelper.GetHostingEnvironment();
|
||||
|
||||
var loggingConfiguration = TestHelper.GetLoggingConfiguration(hostingEnv);
|
||||
|
||||
var exampleLogfilePath = Path.Combine(testRoot, "TestHelpers", "Assets", LogfileName);
|
||||
_newLogfileDirPath = loggingConfiguration.LogDirectory;
|
||||
_newLogfilePath = Path.Combine(_newLogfileDirPath, LogfileName);
|
||||
|
||||
// Create/ensure Directory exists
|
||||
ioHelper.EnsurePathExists(_newLogfileDirPath);
|
||||
|
||||
// Copy the sample files
|
||||
File.Copy(exampleLogfilePath, _newLogfilePath, true);
|
||||
|
||||
var logger = Mock.Of<ILogger<SerilogJsonLogViewer>>();
|
||||
var logViewerConfig = new LogViewerConfig(LogViewerQueryRepository, TestHelper.ScopeProvider);
|
||||
var logLevelLoader = Mock.Of<ILogLevelLoader>();
|
||||
_logViewer =
|
||||
new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, logLevelLoader, Log.Logger);
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Cleanup & delete the example log & search files off disk
|
||||
// Once all tests in this class/fixture have run
|
||||
if (File.Exists(_newLogfilePath))
|
||||
{
|
||||
File.Delete(_newLogfilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private ILogViewer _logViewer;
|
||||
|
||||
private const string LogfileName = "UmbracoTraceLog.UNITTEST.20181112.json";
|
||||
|
||||
private string _newLogfilePath;
|
||||
private string _newLogfileDirPath;
|
||||
|
||||
private readonly LogTimePeriod _logTimePeriod = new(
|
||||
new DateTime(2018, 11, 12, 0, 0, 0),
|
||||
new DateTime(2018, 11, 13, 0, 0, 0));
|
||||
|
||||
private ILogViewerQueryRepository LogViewerQueryRepository { get; } = new TestLogViewerQueryRepository();
|
||||
|
||||
[Test]
|
||||
public void Logs_Contain_Correct_Error_Count()
|
||||
{
|
||||
var numberOfErrors = _logViewer.GetNumberOfErrors(_logTimePeriod);
|
||||
|
||||
// Our dummy log should contain 2 errors
|
||||
Assert.AreEqual(1, numberOfErrors);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Logs_Contain_Correct_Log_Level_Counts()
|
||||
{
|
||||
var logCounts = _logViewer.GetLogLevelCounts(_logTimePeriod);
|
||||
|
||||
Assert.AreEqual(55, logCounts.Debug);
|
||||
Assert.AreEqual(1, logCounts.Error);
|
||||
Assert.AreEqual(0, logCounts.Fatal);
|
||||
Assert.AreEqual(38, logCounts.Information);
|
||||
Assert.AreEqual(6, logCounts.Warning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Logs_Contains_Correct_Message_Templates()
|
||||
{
|
||||
var templates = _logViewer.GetMessageTemplates(_logTimePeriod).ToArray();
|
||||
|
||||
// Count no of templates
|
||||
Assert.AreEqual(25, templates.Count());
|
||||
|
||||
// Verify all templates & counts are unique
|
||||
CollectionAssert.AllItemsAreUnique(templates);
|
||||
|
||||
// Ensure the collection contains LogTemplate objects
|
||||
CollectionAssert.AllItemsAreInstancesOfType(templates, typeof(LogTemplate));
|
||||
|
||||
// Get first item & verify its template & count are what we expect
|
||||
var popularTemplate = templates.FirstOrDefault();
|
||||
|
||||
Assert.IsNotNull(popularTemplate);
|
||||
Assert.AreEqual("{EndMessage} ({Duration}ms) [Timing {TimingId}]", popularTemplate.MessageTemplate);
|
||||
Assert.AreEqual(26, popularTemplate.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Logs_Can_Open_As_Small_File()
|
||||
{
|
||||
// We are just testing a return value (as we know the example file is less than 200MB)
|
||||
// But this test method does not test/check that
|
||||
var canOpenLogs = _logViewer.CheckCanOpenLogs(_logTimePeriod);
|
||||
Assert.IsTrue(canOpenLogs);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Logs_Can_Be_Queried()
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
// Should get me the most 100 recent log entries & using default overloads for remaining params
|
||||
var allLogs = _logViewer.GetLogs(_logTimePeriod, 1);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Check we get 100 results back for a page & total items all good :)
|
||||
Assert.AreEqual(100, allLogs.Items.Count());
|
||||
Assert.AreEqual(102, allLogs.TotalItems);
|
||||
Assert.AreEqual(2, allLogs.TotalPages);
|
||||
|
||||
// Check collection all contain same object type
|
||||
CollectionAssert.AllItemsAreInstancesOfType(allLogs.Items, typeof(LogMessage));
|
||||
|
||||
// Check first item is newest
|
||||
var newestItem = allLogs.Items.First();
|
||||
DateTimeOffset.TryParse("2018-11-12T08:39:18.1971147Z", out var newDate);
|
||||
Assert.AreEqual(newDate, newestItem.Timestamp);
|
||||
|
||||
// Check we call method again with a smaller set of results & in ascending
|
||||
var smallQuery = _logViewer.GetLogs(_logTimePeriod, 1, 10, Direction.Ascending);
|
||||
Assert.AreEqual(10, smallQuery.Items.Count());
|
||||
Assert.AreEqual(11, smallQuery.TotalPages);
|
||||
|
||||
// Check first item is oldest
|
||||
var oldestItem = smallQuery.Items.First();
|
||||
DateTimeOffset.TryParse("2018-11-12T08:34:45.8371142Z", out var oldDate);
|
||||
Assert.AreEqual(oldDate, oldestItem.Timestamp);
|
||||
|
||||
// Check invalid log levels
|
||||
// Rather than expect 0 items - get all items back & ignore the invalid levels
|
||||
string[] invalidLogLevels = { "Invalid", "NotALevel" };
|
||||
var queryWithInvalidLevels = _logViewer.GetLogs(_logTimePeriod, 1, logLevels: invalidLogLevels);
|
||||
Assert.AreEqual(102, queryWithInvalidLevels.TotalItems);
|
||||
|
||||
// Check we can call method with an array of logLevel (error & warning)
|
||||
string[] logLevels = { "Warning", "Error" };
|
||||
var queryWithLevels = _logViewer.GetLogs(_logTimePeriod, 1, logLevels: logLevels);
|
||||
Assert.AreEqual(7, queryWithLevels.TotalItems);
|
||||
|
||||
// Query @Level='Warning' BUT we pass in array of LogLevels for Debug & Info (Expect to get 0 results)
|
||||
string[] logLevelMismatch = { "Debug", "Information" };
|
||||
var filterLevelQuery = _logViewer.GetLogs(
|
||||
_logTimePeriod,
|
||||
1,
|
||||
filterExpression: "@Level='Warning'",
|
||||
logLevels: logLevelMismatch);
|
||||
Assert.AreEqual(0, filterLevelQuery.TotalItems);
|
||||
}
|
||||
|
||||
[TestCase("", 102)]
|
||||
[TestCase("Has(@Exception)", 1)]
|
||||
[TestCase("Has(@x)", 1)]
|
||||
[TestCase("Has(Duration) and Duration > 1000", 2)]
|
||||
[TestCase("Not(@Level = 'Verbose') and Not(@Level = 'Debug')", 45)]
|
||||
[TestCase("Not(@l = 'Verbose') and Not(@l = 'Debug')", 45)]
|
||||
[TestCase("StartsWith(SourceContext, 'Umbraco.Core')", 86)]
|
||||
[TestCase("@MessageTemplate = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 26)]
|
||||
[TestCase("@mt = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 26)]
|
||||
[TestCase("SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", 1)]
|
||||
[TestCase("Contains(SortedComponentTypes[?], 'DatabaseServer')", 1)]
|
||||
[TestCase("@Message like '%definition%'", 6)]
|
||||
[TestCase("definition", 6)]
|
||||
[Test]
|
||||
public void Logs_Can_Query_With_Expressions(string queryToVerify, int expectedCount)
|
||||
{
|
||||
var testQuery = _logViewer.GetLogs(_logTimePeriod, 1, filterExpression: queryToVerify);
|
||||
Assert.AreEqual(expectedCount, testQuery.TotalItems);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Log_Search_Can_Persist()
|
||||
{
|
||||
// Add a new search
|
||||
_logViewer.AddSavedSearch("Unit Test Example", "Has(UnitTest)");
|
||||
|
||||
var searches = _logViewer.GetSavedSearches();
|
||||
|
||||
// Check if we can find the newly added item from the results we get back
|
||||
var findItem = searches.Where(x => x.Name == "Unit Test Example" && x.Query == "Has(UnitTest)");
|
||||
|
||||
Assert.IsNotNull(findItem, "We should have found the saved search, but get no results");
|
||||
Assert.AreEqual(1, findItem.Count(), "Our list of searches should only contain one result");
|
||||
|
||||
// TODO: Need someone to help me find out why these don't work
|
||||
// CollectionAssert.Contains(searches, savedSearch, "Can not find the new search that was saved");
|
||||
// Assert.That(searches, Contains.Item(savedSearch));
|
||||
|
||||
// Remove the search from above & ensure it no longer exists
|
||||
_logViewer.DeleteSavedSearch("Unit Test Example");
|
||||
|
||||
searches = _logViewer.GetSavedSearches();
|
||||
findItem = searches.Where(x => x.Name == "Unit Test Example" && x.Query == "Has(UnitTest)");
|
||||
Assert.IsEmpty(findItem, "The search item should no longer exist");
|
||||
}
|
||||
}
|
||||
|
||||
internal class TestLogViewerQueryRepository : ILogViewerQueryRepository
|
||||
{
|
||||
public TestLogViewerQueryRepository() =>
|
||||
Store = new List<ILogViewerQuery>(DatabaseDataCreator._defaultLogQueries.Select(LogViewerQueryModelFactory.BuildEntity));
|
||||
|
||||
private IList<ILogViewerQuery> Store { get; }
|
||||
|
||||
private LogViewerQueryRepository.LogViewerQueryModelFactory LogViewerQueryModelFactory { get; } = new();
|
||||
|
||||
public ILogViewerQuery Get(int id) => Store.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
public IEnumerable<ILogViewerQuery> GetMany(params int[] ids) =>
|
||||
ids.Any() ? Store.Where(x => ids.Contains(x.Id)) : Store;
|
||||
|
||||
public bool Exists(int id) => Get(id) is not null;
|
||||
|
||||
public void Save(ILogViewerQuery entity)
|
||||
{
|
||||
var item = Get(entity.Id);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
Store.Add(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Name = entity.Name;
|
||||
item.Query = entity.Query;
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(ILogViewerQuery entity)
|
||||
{
|
||||
var item = Get(entity.Id);
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
Store.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ILogViewerQuery> Get(IQuery<ILogViewerQuery> query) => throw new NotImplementedException();
|
||||
|
||||
public int Count(IQuery<ILogViewerQuery> query) => throw new NotImplementedException();
|
||||
|
||||
public ILogViewerQuery GetByName(string name) => Store.FirstOrDefault(x => x.Name == name);
|
||||
}
|
||||
Reference in New Issue
Block a user