diff --git a/src/Umbraco.Core/Services/ILogViewerRepository.cs b/src/Umbraco.Core/Services/ILogViewerRepository.cs index 770099668bc2..6d93928efc11 100644 --- a/src/Umbraco.Core/Services/ILogViewerRepository.cs +++ b/src/Umbraco.Core/Services/ILogViewerRepository.cs @@ -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; +/// +/// Represents a repository for viewing logs in Umbraco. +/// public interface ILogViewerRepository { /// diff --git a/src/Umbraco.Core/Services/LogViewerService.cs b/src/Umbraco.Core/Services/LogViewerService.cs index 0780838416a9..5ca2a087855f 100644 --- a/src/Umbraco.Core/Services/LogViewerService.cs +++ b/src/Umbraco.Core/Services/LogViewerService.cs @@ -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 +/// +/// Represents a service for viewing logs in Umbraco. +/// +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; + /// + /// Initializes a new instance of the class. + /// public LogViewerService( ILogViewerQueryRepository logViewerQueryRepository, ICoreScopeProvider provider, ILoggingConfiguration loggingConfiguration, ILogViewerRepository logViewerRepository) + : base( + logViewerQueryRepository, + provider, + logViewerRepository) { - _logViewerQueryRepository = logViewerQueryRepository; - _provider = provider; _loggingConfiguration = loggingConfiguration; - _logViewerRepository = logViewerRepository; - } - - /// - public Task?, 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?, LogViewerOperationStatus>( - LogViewerOperationStatus.CancelledByLogsSizeValidation, - null)); - } - - - PagedModel filteredLogs = GetFilteredLogs(logTimePeriod, filterExpression, logLevels, orderDirection, skip, take); - - return Task.FromResult(Attempt.SucceedWithStatus?, LogViewerOperationStatus>( - LogViewerOperationStatus.Success, - filteredLogs)); } /// - public Task> GetSavedLogQueriesAsync(int skip, int take) - { - using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); - ILogViewerQuery[] savedLogQueries = _logViewerQueryRepository.GetMany().ToArray(); - var pagedModel = new PagedModel(savedLogQueries.Length, savedLogQueries.Skip(skip).Take(take)); - return Task.FromResult(pagedModel); - } + protected override string LoggerName => "UmbracoFile"; /// - public Task GetSavedLogQueryByNameAsync(string name) + public override Task> CanViewLogsAsync(LogTimePeriod logTimePeriod) { - using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); - return Task.FromResult(_logViewerQueryRepository.GetByName(name)); - } - - /// - public async Task> AddSavedLogQueryAsync(string name, string query) - { - ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name); - - if (logViewerQuery is not null) - { - return Attempt.FailWithStatus(LogViewerOperationStatus.DuplicateLogSearch, null); - } - - logViewerQuery = new LogViewerQuery(name, query); - - using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); - _logViewerQueryRepository.Save(logViewerQuery); - - return Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, logViewerQuery); - } - - /// - public async Task> DeleteSavedLogQueryAsync(string name) - { - ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name); - - if (logViewerQuery is null) - { - return Attempt.FailWithStatus(LogViewerOperationStatus.NotFoundLogSearch, null); - } - - using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); - _logViewerQueryRepository.Delete(logViewerQuery); - - return Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, logViewerQuery); - } - - /// - public Task> 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)); - } - - /// - public Task> 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( + return Task.FromResult(Attempt.FailWithStatus( LogViewerOperationStatus.CancelledByLogsSizeValidation, - null)); - } - - LogLevelCounts counter = _logViewerRepository.GetLogCount(logTimePeriod); - - return Task.FromResult(Attempt.SucceedWithStatus( - LogViewerOperationStatus.Success, - counter)); - } - - /// - public Task, 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, LogViewerOperationStatus>( - LogViewerOperationStatus.CancelledByLogsSizeValidation, - null!)); - } - - LogTemplate[] messageTemplates = _logViewerRepository.GetMessageTemplates(logTimePeriod); - - return Task.FromResult(Attempt.SucceedWithStatus( - LogViewerOperationStatus.Success, - new PagedModel(messageTemplates.Length, messageTemplates.Skip(skip).Take(take)))); - } - - /// - public ReadOnlyDictionary GetLogLevelsFromSinks() - { - var configuredLogLevels = new Dictionary - { - { "Global", GetGlobalMinLogLevel() }, - { "UmbracoFile", _logViewerRepository.RestrictedToMinimumLevel() }, - }; - - return configuredLogLevels.AsReadOnly(); - } - - /// - public LogLevel GetGlobalMinLogLevel() => _logViewerRepository.GetGlobalMinLogLevel(); - - /// - /// Returns a representation from a start and end date for filtering log files. - /// - /// The start date for the date range (can be null). - /// The end date for the date range (can be null). - /// The LogTimePeriod object used to filter logs. - 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)); } /// /// Returns a value indicating whether to stop a GET request that is attempting to fetch logs from a 1GB file. /// /// The time period to filter the logs. - /// The value whether or not you are able to view the logs. + /// Whether you are able to view the logs. private bool CanViewLogs(LogTimePeriod logTimePeriod) { // Number of entries @@ -230,52 +76,5 @@ private bool CanViewLogs(LogTimePeriod logTimePeriod) return logSizeAsMegabytes <= FileSizeCap; } - private PagedModel GetFilteredLogs( - LogTimePeriod logTimePeriod, - string? filterExpression, - string[]? logLevels, - Direction orderDirection, - int skip, - int take) - { - IEnumerable 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(); - 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 - { - 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"; } diff --git a/src/Umbraco.Core/Services/LogViewerServiceBase.cs b/src/Umbraco.Core/Services/LogViewerServiceBase.cs new file mode 100644 index 000000000000..0b69e964e17d --- /dev/null +++ b/src/Umbraco.Core/Services/LogViewerServiceBase.cs @@ -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; + +/// +/// Base class for log viewer services that provides common functionality for managing log entries and queries. +/// +public abstract class LogViewerServiceBase : ILogViewerService +{ + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly ICoreScopeProvider _provider; + + /// + /// Initializes a new instance of the class. + /// + protected LogViewerServiceBase( + ILogViewerQueryRepository logViewerQueryRepository, + ICoreScopeProvider provider, + ILogViewerRepository logViewerRepository) + { + _logViewerQueryRepository = logViewerQueryRepository; + _provider = provider; + LogViewerRepository = logViewerRepository; + } + + /// + /// Gets the . + /// + protected ILogViewerRepository LogViewerRepository { get; } + + /// + /// Gets the name of the logger. + /// + protected abstract string LoggerName { get; } + + /// + public virtual ReadOnlyDictionary GetLogLevelsFromSinks() + { + var configuredLogLevels = new Dictionary + { + { "Global", GetGlobalMinLogLevel() }, + { LoggerName, LogViewerRepository.RestrictedToMinimumLevel() }, + }; + + return configuredLogLevels.AsReadOnly(); + } + + /// + public virtual LogLevel GetGlobalMinLogLevel() => LogViewerRepository.GetGlobalMinLogLevel(); + + /// + public virtual Task GetSavedLogQueryByNameAsync(string name) + { + using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); + return Task.FromResult(_logViewerQueryRepository.GetByName(name)); + } + + /// + public virtual async Task> AddSavedLogQueryAsync(string name, string query) + { + ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name); + + if (logViewerQuery is not null) + { + return Attempt.FailWithStatus( + LogViewerOperationStatus.DuplicateLogSearch, null); + } + + logViewerQuery = new LogViewerQuery(name, query); + + using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); + _logViewerQueryRepository.Save(logViewerQuery); + + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, + logViewerQuery); + } + + /// + public virtual async Task> DeleteSavedLogQueryAsync(string name) + { + ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name); + + if (logViewerQuery is null) + { + return Attempt.FailWithStatus( + LogViewerOperationStatus.NotFoundLogSearch, null); + } + + using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); + _logViewerQueryRepository.Delete(logViewerQuery); + + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, + logViewerQuery); + } + + /// + public virtual Task> GetSavedLogQueriesAsync(int skip, int take) + { + using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); + ILogViewerQuery[] savedLogQueries = _logViewerQueryRepository.GetMany().ToArray(); + var pagedModel = new PagedModel(savedLogQueries.Length, savedLogQueries.Skip(skip).Take(take)); + return Task.FromResult(pagedModel); + } + + /// + public virtual async Task> GetLogLevelCountsAsync( + DateTimeOffset? startDate, DateTimeOffset? endDate) + { + LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate); + + Attempt 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( + LogViewerOperationStatus.CancelledByLogsSizeValidation, + null); + } + + LogLevelCounts counter = LogViewerRepository.GetLogCount(logTimePeriod); + + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, + counter); + } + + /// + public virtual async Task, LogViewerOperationStatus>> GetMessageTemplatesAsync( + DateTimeOffset? startDate, DateTimeOffset? endDate, int skip, int take) + { + LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate); + + Attempt 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, LogViewerOperationStatus>( + LogViewerOperationStatus.CancelledByLogsSizeValidation, + null!); + } + + LogTemplate[] messageTemplates = LogViewerRepository.GetMessageTemplates(logTimePeriod); + + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, + new PagedModel(messageTemplates.Length, messageTemplates.Skip(skip).Take(take))); + } + + /// + public virtual async Task?, 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 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?, LogViewerOperationStatus>( + LogViewerOperationStatus.CancelledByLogsSizeValidation, + null); + } + + PagedModel filteredLogs = + GetFilteredLogs(logTimePeriod, filterExpression, logLevels, orderDirection, skip, take); + + return Attempt.SucceedWithStatus?, LogViewerOperationStatus>( + LogViewerOperationStatus.Success, + filteredLogs); + } + + /// + public virtual Task> CanViewLogsAsync( + DateTimeOffset? startDate, + DateTimeOffset? endDate) + => CanViewLogsAsync(GetTimePeriod(startDate, endDate)); + + /// + /// Checks if the logs for the specified time period can be viewed. + /// + public abstract Task> CanViewLogsAsync(LogTimePeriod logTimePeriod); + + + /// + /// Returns a representation from a start and end date for filtering log files. + /// + /// The start date for the date range (can be null). + /// The end date for the date range (can be null). + /// The LogTimePeriod object used to filter logs. + 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); + } + + /// + /// Gets a filtered page of logs from storage based on the provided parameters. + /// + protected PagedModel GetFilteredLogs( + LogTimePeriod logTimePeriod, + string? filterExpression, + string[]? logLevels, + Direction orderDirection, + int skip, + int take) + { + IEnumerable 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(); + 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 + { + Total = logs.Count(), + Items = logs + .OrderBy(l => l.Timestamp, orderDirection) + .Skip(skip) + .Take(take), + }; + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs index f899f311f5ce..19b6bc05d5be 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs @@ -195,6 +195,7 @@ public static IUmbracoBuilder ConfigureFileSystems( /// /// The type of the log viewer. /// The builder. + [Obsolete("No longer used. Scheduled removal in Umbraco 18.")] public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) where T : class, ILogViewer { @@ -207,6 +208,7 @@ public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) /// /// The builder. /// A function creating a log viewer. + [Obsolete("No longer used. Scheduled removal in Umbraco 18.")] public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) { builder.Services.AddUnique(factory); @@ -218,6 +220,7 @@ public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func /// A builder. /// A log viewer. + [Obsolete("No longer used. Scheduled removal in Umbraco 18.")] public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) { builder.Services.AddUnique(viewer); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 9d3213bd2a9b..4006c7ca1642 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -22,7 +22,7 @@ public static class LoggerConfigExtensions /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - [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 @@ public static LoggerConfiguration MinimalConfiguration( /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - [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 @@ public static LoggerConfiguration MinimalConfiguration( 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 @@ public static LoggerConfiguration MinimalConfiguration( return logConfig; } - /// /// This configures Serilog with some defaults /// Such as adding ProcessID, Thread, AppDomain etc @@ -108,14 +107,17 @@ public static LoggerConfiguration MinimalConfiguration( .Enrich.With() .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 @@ public static LoggerConfiguration MinimalConfiguration( /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// - [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 @@ public static LoggerConfiguration OutputDefaultTextFile( //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 @@ public static LoggerConfiguration UmbracoFile( null)); } - /// /// Outputs a CLEF format JSON log at /App_Data/Logs/ /// @@ -222,7 +223,7 @@ public static LoggerConfiguration UmbracoFile( /// The log level you wish the JSON file to collect - default is Verbose (highest) /// /// The number of days to keep log files. Default is set to null which means all logs are kept - [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 @@ public static LoggerConfiguration OutputDefaultJsonFile( // 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 @@ public static LoggerConfiguration OutputDefaultJsonFile( return logConfig; } - } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 4eb054b2a5de..2b91932f8b80 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -19,7 +19,7 @@ public SerilogLogger(LoggerConfiguration logConfig) => 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 static SerilogLogger CreateWithDefaultConfiguration( /// Creates a logger with some pre-defined configuration and remainder from config file /// /// Used by UmbracoApplicationBase to get its logger. - [Obsolete] + [Obsolete("Scheduled for removal in Umbraco 17.")] public static SerilogLogger CreateWithDefaultConfiguration( IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration, diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs index 5d650dac07c2..33a6b549afdb 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs @@ -22,6 +22,7 @@ public UmbracoFileConfiguration(IConfiguration configuration) { 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 UmbracoFileConfiguration(IConfiguration configuration) } } + public bool Enabled { get; set; } = true; + public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; public long FileSizeLimitBytes { get; set; } = 1073741824; diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 00d0f0517d93..82d2277dc8ca 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Core.Logging.Viewer; +[Obsolete("Use ILogViewerService instead. Scheduled removal in Umbraco 18.")] public interface ILogViewer { bool CanHandleLargeLogs { get; } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs deleted file mode 100644 index 9c8dace1cf1d..000000000000 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs +++ /dev/null @@ -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 _logger; - private readonly string _logsPath; - - public SerilogJsonLogViewer( - ILogger 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 GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, - int take) - { - var logs = new List(); - - 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; - } - } -} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index d2f21cf5d0cf..cd485e766071 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.Logging.Viewer; +[Obsolete("Use ILogViewerService instead. Scheduled removal in Umbraco 18.")] public abstract class SerilogLogViewerSourceBase : ILogViewer { private readonly ILogLevelLoader _logLevelLoader; diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs index e1aac6486a41..70f4588922ab 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs @@ -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 +/// +/// Repository for accessing log entries from the Umbraco log files stored on disk. +/// +public class LogViewerRepository : LogViewerRepositoryBase { private readonly ILoggingConfiguration _loggingConfiguration; private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; - private readonly UmbracoFileConfiguration _umbracoFileConfig; - public LogViewerRepository(ILoggingConfiguration loggingConfiguration, ILogger logger, IJsonSerializer jsonSerializer, UmbracoFileConfiguration umbracoFileConfig) + /// + /// Initializes a new instance of the class. + /// + public LogViewerRepository( + ILoggingConfiguration loggingConfiguration, + ILogger logger, + IJsonSerializer jsonSerializer, + UmbracoFileConfiguration umbracoFileConfig) + : base(umbracoFileConfig) { _loggingConfiguration = loggingConfiguration; _logger = logger; _jsonSerializer = jsonSerializer; - _umbracoFileConfig = umbracoFileConfig; } - /// - public IEnumerable GetLogs(LogTimePeriod logTimePeriod, string? filterExpression = null) - { - var expressionFilter = new ExpressionFilter(filterExpression); - - return GetLogs(logTimePeriod, expressionFilter); - } - - /// - public LogLevelCounts GetLogCount(LogTimePeriod logTimePeriod) - { - var counter = new CountingFilter(); - - GetLogs(logTimePeriod, counter); - - return counter.Counts; - } - - /// - 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(); - } - - /// - public LogLevel GetGlobalMinLogLevel() - { - LogEventLevel logLevel = GetGlobalLogLevelEventMinLevel(); - - return Enum.Parse(logLevel.ToString()); - } - - public LogLevel RestrictedToMinimumLevel() - { - LogEventLevel minLevel = _umbracoFileConfig.RestrictedToMinimumLevel; - return Enum.Parse(minLevel.ToString()); - } - - private LogEventLevel GetGlobalLogLevelEventMinLevel() => - Enum.GetValues(typeof(LogEventLevel)) - .Cast() - .Where(Log.IsEnabled) - .DefaultIfEmpty(LogEventLevel.Information) - .Min(); - - private IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter) + /// + protected override IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter) { var logs = new List(); @@ -163,7 +120,7 @@ private IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter l 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) { diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs new file mode 100644 index 000000000000..95d650ac6a23 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs @@ -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; + +/// +/// Provides a base class for log viewer repository implementations. +/// +public abstract class LogViewerRepositoryBase : ILogViewerRepository +{ + private readonly UmbracoFileConfiguration _umbracoFileConfig; + + /// + /// Initializes a new instance of the class. + /// + /// + public LogViewerRepositoryBase(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; + + /// + public virtual IEnumerable GetLogs(LogTimePeriod logTimePeriod, string? filterExpression = null) + { + var expressionFilter = new ExpressionFilter(filterExpression); + + return GetLogs(logTimePeriod, expressionFilter); + } + + /// + public virtual LogLevelCounts GetLogCount(LogTimePeriod logTimePeriod) + { + var counter = new CountingFilter(); + + GetLogs(logTimePeriod, counter); + + return counter.Counts; + } + + /// + 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(); + } + + /// + public virtual LogLevel GetGlobalMinLogLevel() + { + LogEventLevel logLevel = GetGlobalLogLevelEventMinLevel(); + + return Enum.Parse(logLevel.ToString()); + } + + /// + /// Gets the minimum-level log value from the config file. + /// + public virtual LogLevel RestrictedToMinimumLevel() + { + LogEventLevel minLevel = _umbracoFileConfig.RestrictedToMinimumLevel; + return Enum.Parse(minLevel.ToString()); + } + + /// + /// Gets the minimum log level from the global Serilog configuration. + /// + protected virtual LogEventLevel GetGlobalLogLevelEventMinLevel() => + Enum.GetValues(typeof(LogEventLevel)) + .Cast() + .Where(Log.IsEnabled) + .DefaultIfEmpty(LogEventLevel.Information) + .Min(); + + /// + /// Retrieves the logs for a specified time period and filter. + /// + protected abstract IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs deleted file mode 100644 index fe199b3cf454..000000000000 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs +++ /dev/null @@ -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>(); - var logViewerConfig = new LogViewerConfig(LogViewerQueryRepository, TestHelper.ScopeProvider); - var logLevelLoader = Mock.Of(); - _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(DatabaseDataCreator._defaultLogQueries.Select(LogViewerQueryModelFactory.BuildEntity)); - - private IList Store { get; } - - private LogViewerQueryRepository.LogViewerQueryModelFactory LogViewerQueryModelFactory { get; } = new(); - - public ILogViewerQuery Get(int id) => Store.FirstOrDefault(x => x.Id == id); - - public IEnumerable 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 Get(IQuery query) => throw new NotImplementedException(); - - public int Count(IQuery query) => throw new NotImplementedException(); - - public ILogViewerQuery GetByName(string name) => Store.FirstOrDefault(x => x.Name == name); -}