From 2287d524040463d0825c20dc453767aa35ebb94b Mon Sep 17 00:00:00 2001 From: Migaroez Date: Tue, 22 Jul 2025 12:24:12 +0200 Subject: [PATCH 01/13] Cleanup obsoleted methods --- .../Logging/Serilog/LoggerConfigExtensions.cs | 120 ------------------ .../Logging/Serilog/SerilogLogger.cs | 25 ---- 2 files changed, 145 deletions(-) diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 9d3213bd2a9b..b18452dcabed 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -17,69 +17,6 @@ namespace Umbraco.Extensions { public static class LoggerConfigExtensions { - /// - /// This configures Serilog with some defaults - /// 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.")] - public static LoggerConfiguration MinimalConfiguration( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) - { - return MinimalConfiguration(logConfig, hostingEnvironment, loggingConfiguration, configuration, out _); - } - - /// - /// This configures Serilog with some defaults - /// 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.")] - public static LoggerConfiguration MinimalConfiguration( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration, - out UmbracoFileConfiguration umbFileConfiguration) - { - Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); - - //Set this environment variable - so that it can be used in external config file - //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> - Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.MapPathContentRoot("/").TrimEnd(Path.DirectorySeparatorChar), EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); - - logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) - .Enrich.WithProcessId() - .Enrich.WithProcessName() - .Enrich.WithThreadId() - .Enrich.WithProperty("ApplicationId", hostingEnvironment.ApplicationId) // Updated later by ApplicationIdEnricher - .Enrich.WithProperty("MachineName", Environment.MachineName) - .Enrich.With() - .Enrich.FromLogContext(); // allows us to dynamically enrich - - //This is not optimal, but seems to be the only way if we do not make an Serilog.Sink.UmbracoFile sink all the way. - var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); - - umbFileConfiguration = umbracoFileConfiguration; - - logConfig.WriteTo.UmbracoFile( - path : umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), - fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, - restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel, - rollingInterval: umbracoFileConfiguration.RollingInterval, - flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, - rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit); - - return logConfig; - } - - /// /// This configures Serilog with some defaults /// Such as adding ProcessID, Thread, AppDomain etc @@ -120,32 +57,6 @@ public static LoggerConfiguration MinimalConfiguration( return logConfig; } - /// - /// Outputs a .txt format log at /App_Data/Logs/ - /// - /// A Serilog LoggerConfiguration - /// - /// The log level you wish the JSON file to collect - default is Verbose (highest) - /// - [Obsolete("Will be removed in Umbraco 13.")] - public static LoggerConfiguration OutputDefaultTextFile( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - LogEventLevel minimumLevel = LogEventLevel.Verbose) - { - //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"), - shared: true, - rollingInterval: RollingInterval.Day, - restrictedToMinimumLevel: minimumLevel, - retainedFileCountLimit: null, //Setting to null means we keep all files - default is 31 days - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}"); - - return logConfig; - } - /// /// Outputs a .txt format log at /App_Data/Logs/ /// @@ -213,36 +124,6 @@ public static LoggerConfiguration UmbracoFile( null)); } - - /// - /// Outputs a CLEF format JSON log at /App_Data/Logs/ - /// - /// A Serilog LoggerConfiguration - /// The logging configuration - /// 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.")] - public static LoggerConfiguration OutputDefaultJsonFile( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - LogEventLevel minimumLevel = LogEventLevel.Verbose, - int? retainedFileCount = null) - { - // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) - // 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"), - 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 - restrictedToMinimumLevel: minimumLevel); - - return logConfig; - } - /// /// Outputs a CLEF format JSON log at /App_Data/Logs/ /// @@ -270,6 +151,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..da8ed26e3625 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -19,33 +19,8 @@ public SerilogLogger(LoggerConfiguration logConfig) => public ILogger SerilogLog { get; } - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) => - CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); - public void Dispose() => SerilogLog.DisposeIfDisposable(); - /// - /// Creates a logger with some pre-defined configuration and remainder from config file - /// - /// Used by UmbracoApplicationBase to get its logger. - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration, - out UmbracoFileConfiguration umbracoFileConfig) - { - LoggerConfiguration? serilogConfig = new LoggerConfiguration() - .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) - .ReadFrom.Configuration(configuration); - - return new SerilogLogger(serilogConfig); - } - public bool IsEnabled(Type reporting, LogLevel level) => LoggerFor(reporting).IsEnabled(MapLevel(level)); From 8137426be13bf4c6265b280cf24faeb4bfd389eb Mon Sep 17 00:00:00 2001 From: Migaroez Date: Thu, 24 Jul 2025 08:57:50 +0200 Subject: [PATCH 02/13] Add a way to disable UmbracoFile default sink --- .../Logging/Serilog/LoggerConfigExtensions.cs | 19 +++++++++++-------- .../Serilog/UmbracoFileConfiguration.cs | 3 +++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index b18452dcabed..7f9fce6e4600 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -45,14 +45,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; } 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; From a4049fd82f1a49b0468fd2f336de88896fc373d2 Mon Sep 17 00:00:00 2001 From: Migaroez Date: Thu, 24 Jul 2025 13:15:39 +0200 Subject: [PATCH 03/13] Abstract LogViewService so only UmbracoFile sink related things are in the default interface implementation. --- src/Umbraco.Core/Services/LogViewerService.cs | 230 +--------------- .../Services/LogViewerServiceBase.cs | 256 ++++++++++++++++++ 2 files changed, 266 insertions(+), 220 deletions(-) create mode 100644 src/Umbraco.Core/Services/LogViewerServiceBase.cs diff --git a/src/Umbraco.Core/Services/LogViewerService.cs b/src/Umbraco.Core/Services/LogViewerService.cs index 0780838416a9..c260656e90fb 100644 --- a/src/Umbraco.Core/Services/LogViewerService.cs +++ b/src/Umbraco.Core/Services/LogViewerService.cs @@ -1,212 +1,49 @@ -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 +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; 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); - } - - /// - public Task GetSavedLogQueryByNameAsync(string name) - { - 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); + protected override string RepositoryLoggerName => "UmbracoFile"; - return Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, logViewerQuery); - } - - /// - public Task> CanViewLogsAsync(DateTimeOffset? startDate, DateTimeOffset? endDate) + public override Task> CanViewLogsAsync(LogTimePeriod logTimePeriod) { - 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)); } - /// - 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( - 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; - } - } - - return new LogTimePeriod(startDate.Value.LocalDateTime, endDate.Value.LocalDateTime); - } - /// /// 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 +67,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"; } diff --git a/src/Umbraco.Core/Services/LogViewerServiceBase.cs b/src/Umbraco.Core/Services/LogViewerServiceBase.cs new file mode 100644 index 000000000000..c90ecf16afc3 --- /dev/null +++ b/src/Umbraco.Core/Services/LogViewerServiceBase.cs @@ -0,0 +1,256 @@ +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; + +public abstract class LogViewerServiceBase : ILogViewerService +{ + private readonly ILogViewerRepository _logViewerRepository; + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly ICoreScopeProvider _provider; + + protected LogViewerServiceBase( + ILogViewerQueryRepository logViewerQueryRepository, + ICoreScopeProvider provider, + ILogViewerRepository logViewerRepository) + { + _logViewerQueryRepository = logViewerQueryRepository; + _provider = provider; + _logViewerRepository = logViewerRepository; + } + + protected abstract string RepositoryLoggerName { get; } + + /// + public virtual ReadOnlyDictionary GetLogLevelsFromSinks() + { + var configuredLogLevels = new Dictionary + { + { "Global", GetGlobalMinLogLevel() }, + { RepositoryLoggerName, _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)); + + 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; + if (startDate is null) + { + startDate = now.AddDays(-1); + } + + if (endDate is null) + { + endDate = now; + } + } + + return new LogTimePeriod(startDate.Value.LocalDateTime, endDate.Value.LocalDateTime); + } + + 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), + }; + } +} From 78abf2eb83ac45630ec6b180ea81b6f500f46403 Mon Sep 17 00:00:00 2001 From: Migaroez Date: Thu, 24 Jul 2025 13:28:01 +0200 Subject: [PATCH 04/13] Abstract LogViewRepository so only UmbracoFile sink related things are in the default interface implementation. --- .../Services/Implement/LogViewerRepository.cs | 60 +++-------------- .../Implement/LogViewerRepositoryBase.cs | 65 +++++++++++++++++++ 2 files changed, 74 insertions(+), 51 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs index e1aac6486a41..e90d422110ac 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs @@ -5,79 +5,37 @@ 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 +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) + 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() => + protected override 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(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs new file mode 100644 index 000000000000..54f3984d5646 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs @@ -0,0 +1,65 @@ +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; + +public abstract class LogViewerRepositoryBase : ILogViewerRepository +{ + private readonly UmbracoFileConfiguration _umbracoFileConfig; + + 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()); + } + + public virtual LogLevel RestrictedToMinimumLevel() + { + LogEventLevel minLevel = _umbracoFileConfig.RestrictedToMinimumLevel; + return Enum.Parse(minLevel.ToString()); + } + + protected abstract LogEventLevel GetGlobalLogLevelEventMinLevel(); + + protected abstract IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter); +} From 43b07ca09933bdb1e070882205d92467e1fb2d64 Mon Sep 17 00:00:00 2001 From: Migaroez Date: Mon, 28 Jul 2025 21:49:56 +0200 Subject: [PATCH 05/13] Move GetGlobalLogLevelEventMinLevel to base --- .../Services/Implement/LogViewerRepository.cs | 7 ------- .../Services/Implement/LogViewerRepositoryBase.cs | 10 ++++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs index e90d422110ac..d69d3eaf5f06 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs @@ -28,13 +28,6 @@ public LogViewerRepository( _jsonSerializer = jsonSerializer; } - protected override LogEventLevel GetGlobalLogLevelEventMinLevel() => - Enum.GetValues(typeof(LogEventLevel)) - .Cast() - .Where(Log.IsEnabled) - .DefaultIfEmpty(LogEventLevel.Information) - .Min(); - protected override IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter) { var logs = new List(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs index 54f3984d5646..726891336ceb 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs @@ -1,4 +1,5 @@ -using Serilog.Events; +using Serilog; +using Serilog.Events; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Logging.Serilog; @@ -59,7 +60,12 @@ public virtual LogLevel RestrictedToMinimumLevel() return Enum.Parse(minLevel.ToString()); } - protected abstract LogEventLevel GetGlobalLogLevelEventMinLevel(); + protected virtual LogEventLevel GetGlobalLogLevelEventMinLevel() => + Enum.GetValues(typeof(LogEventLevel)) + .Cast() + .Where(Log.IsEnabled) + .DefaultIfEmpty(LogEventLevel.Information) + .Min(); protected abstract IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter); } From bac9fd888b2ecb116e86a07285bb6d12a5b02b6a Mon Sep 17 00:00:00 2001 From: Migaroez Date: Tue, 29 Jul 2025 14:37:56 +0200 Subject: [PATCH 06/13] Removed unused internal class and obsoleted its base --- .../Logging/Viewer/SerilogJsonLogViewer.cs | 133 ------------------ .../Viewer/SerilogLogViewerSourceBase.cs | 1 + 2 files changed, 1 insertion(+), 133 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs 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..565e20de39a3 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 v18")] public abstract class SerilogLogViewerSourceBase : ILogViewer { private readonly ILogLevelLoader _logLevelLoader; From 916d88df78cb2d697fc2929d695d61da0f6e9db2 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 30 Jul 2025 06:47:56 +0200 Subject: [PATCH 07/13] Added missing XML header comments and resolved warnings in service and repository classes. --- .../Services/ILogViewerRepository.cs | 5 ++- src/Umbraco.Core/Services/LogViewerService.cs | 11 +++++- .../Services/LogViewerServiceBase.cs | 35 ++++++++++++------- .../Services/Implement/LogViewerRepository.cs | 12 +++++-- .../Implement/LogViewerRepositoryBase.cs | 23 +++++++++--- 5 files changed, 63 insertions(+), 23 deletions(-) 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 c260656e90fb..a586aee24079 100644 --- a/src/Umbraco.Core/Services/LogViewerService.cs +++ b/src/Umbraco.Core/Services/LogViewerService.cs @@ -6,11 +6,17 @@ namespace Umbraco.Cms.Core.Services; +/// +/// Represents a service for viewing logs in Umbraco. +/// public class LogViewerService : LogViewerServiceBase { private const int FileSizeCap = 100; private readonly ILoggingConfiguration _loggingConfiguration; + /// + /// Initializes a new instance of the class. + /// public LogViewerService( ILogViewerQueryRepository logViewerQueryRepository, ICoreScopeProvider provider, @@ -24,15 +30,18 @@ public LogViewerService( _loggingConfiguration = loggingConfiguration; } + /// protected override string RepositoryLoggerName => "UmbracoFile"; + /// public override Task> CanViewLogsAsync(LogTimePeriod logTimePeriod) { bool isAllowed = CanViewLogs(logTimePeriod); if (isAllowed == false) { - return Task.FromResult(Attempt.FailWithStatus(LogViewerOperationStatus.CancelledByLogsSizeValidation, + return Task.FromResult(Attempt.FailWithStatus( + LogViewerOperationStatus.CancelledByLogsSizeValidation, isAllowed)); } diff --git a/src/Umbraco.Core/Services/LogViewerServiceBase.cs b/src/Umbraco.Core/Services/LogViewerServiceBase.cs index c90ecf16afc3..72f0ebfa8c69 100644 --- a/src/Umbraco.Core/Services/LogViewerServiceBase.cs +++ b/src/Umbraco.Core/Services/LogViewerServiceBase.cs @@ -9,12 +9,18 @@ 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 ILogViewerRepository _logViewerRepository; private readonly ILogViewerQueryRepository _logViewerQueryRepository; private readonly ICoreScopeProvider _provider; + /// + /// Initializes a new instance of the class. + /// protected LogViewerServiceBase( ILogViewerQueryRepository logViewerQueryRepository, ICoreScopeProvider provider, @@ -25,6 +31,9 @@ protected LogViewerServiceBase( _logViewerRepository = logViewerRepository; } + /// + /// Gets the name of the logger. + /// protected abstract string RepositoryLoggerName { get; } /// @@ -50,8 +59,7 @@ public virtual ReadOnlyDictionary GetLogLevelsFromSinks() } /// - public virtual async Task> AddSavedLogQueryAsync(string name, - string query) + public virtual async Task> AddSavedLogQueryAsync(string name, string query) { ILogViewerQuery? logViewerQuery = await GetSavedLogQueryByNameAsync(name); @@ -66,7 +74,8 @@ public virtual ReadOnlyDictionary GetLogLevelsFromSinks() using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); _logViewerQueryRepository.Save(logViewerQuery); - return Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, logViewerQuery); } @@ -84,7 +93,8 @@ public virtual ReadOnlyDictionary GetLogLevelsFromSinks() using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); _logViewerQueryRepository.Delete(logViewerQuery); - return Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, logViewerQuery); } @@ -179,6 +189,9 @@ public virtual Task> CanViewLogsAsync( DateTimeOffset? endDate) => CanViewLogsAsync(GetTimePeriod(startDate, endDate)); + /// + /// Checks if the logs for the specified time period can be viewed. + /// public abstract Task> CanViewLogsAsync(LogTimePeriod logTimePeriod); @@ -193,20 +206,16 @@ protected virtual LogTimePeriod GetTimePeriod(DateTimeOffset? startDate, DateTim 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; - } + 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, diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs index d69d3eaf5f06..70f4588922ab 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepository.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using Serilog; +using Microsoft.Extensions.Logging; using Serilog.Events; using Serilog.Formatting.Compact.Reader; using Umbraco.Cms.Core.Logging; @@ -10,12 +9,18 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement; +/// +/// 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; + /// + /// Initializes a new instance of the class. + /// public LogViewerRepository( ILoggingConfiguration loggingConfiguration, ILogger logger, @@ -28,6 +33,7 @@ public LogViewerRepository( _jsonSerializer = jsonSerializer; } + /// protected override IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter) { var logs = new List(); @@ -114,7 +120,7 @@ protected override IEnumerable GetLogs(LogTimePeriod logTimePeriod, I 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 index 726891336ceb..95d650ac6a23 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerRepositoryBase.cs @@ -1,4 +1,4 @@ -using Serilog; +using Serilog; using Serilog.Events; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Services; @@ -7,14 +7,18 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement; +/// +/// Provides a base class for log viewer repository implementations. +/// public abstract class LogViewerRepositoryBase : ILogViewerRepository { private readonly UmbracoFileConfiguration _umbracoFileConfig; - public LogViewerRepositoryBase(UmbracoFileConfiguration umbracoFileConfig) - { - _umbracoFileConfig = umbracoFileConfig; - } + /// + /// Initializes a new instance of the class. + /// + /// + public LogViewerRepositoryBase(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; /// public virtual IEnumerable GetLogs(LogTimePeriod logTimePeriod, string? filterExpression = null) @@ -54,12 +58,18 @@ public virtual LogLevel GetGlobalMinLogLevel() 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() @@ -67,5 +77,8 @@ protected virtual LogEventLevel GetGlobalLogLevelEventMinLevel() => .DefaultIfEmpty(LogEventLevel.Information) .Min(); + /// + /// Retrieves the logs for a specified time period and filter. + /// protected abstract IEnumerable GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter); } From dd29ed01c53fa01c0a74b9d7ad9aed41402509ba Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 30 Jul 2025 07:48:48 +0200 Subject: [PATCH 08/13] Made private method static. --- src/Umbraco.Core/Services/LogViewerService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/LogViewerService.cs b/src/Umbraco.Core/Services/LogViewerService.cs index a586aee24079..d28dc882a618 100644 --- a/src/Umbraco.Core/Services/LogViewerService.cs +++ b/src/Umbraco.Core/Services/LogViewerService.cs @@ -76,5 +76,5 @@ private bool CanViewLogs(LogTimePeriod logTimePeriod) return logSizeAsMegabytes <= FileSizeCap; } - private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json"; + private static string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json"; } From 46a61c19f54cb4f8450504c1c67d0fbfc558fc47 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 1 Aug 2025 08:09:11 +0200 Subject: [PATCH 09/13] Addressed issues raised in code review. --- src/Umbraco.Core/Services/LogViewerService.cs | 2 +- .../Services/LogViewerServiceBase.cs | 4 +- .../Logging/Serilog/LoggerConfigExtensions.cs | 117 ++++++++++++++++++ .../Viewer/SerilogLogViewerSourceBase.cs | 2 +- 4 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Services/LogViewerService.cs b/src/Umbraco.Core/Services/LogViewerService.cs index d28dc882a618..5ca2a087855f 100644 --- a/src/Umbraco.Core/Services/LogViewerService.cs +++ b/src/Umbraco.Core/Services/LogViewerService.cs @@ -31,7 +31,7 @@ public LogViewerService( } /// - protected override string RepositoryLoggerName => "UmbracoFile"; + protected override string LoggerName => "UmbracoFile"; /// public override Task> CanViewLogsAsync(LogTimePeriod logTimePeriod) diff --git a/src/Umbraco.Core/Services/LogViewerServiceBase.cs b/src/Umbraco.Core/Services/LogViewerServiceBase.cs index 72f0ebfa8c69..fbbae29dc6ba 100644 --- a/src/Umbraco.Core/Services/LogViewerServiceBase.cs +++ b/src/Umbraco.Core/Services/LogViewerServiceBase.cs @@ -34,7 +34,7 @@ protected LogViewerServiceBase( /// /// Gets the name of the logger. /// - protected abstract string RepositoryLoggerName { get; } + protected abstract string LoggerName { get; } /// public virtual ReadOnlyDictionary GetLogLevelsFromSinks() @@ -42,7 +42,7 @@ public virtual ReadOnlyDictionary GetLogLevelsFromSinks() var configuredLogLevels = new Dictionary { { "Global", GetGlobalMinLogLevel() }, - { RepositoryLoggerName, _logViewerRepository.RestrictedToMinimumLevel() }, + { LoggerName, _logViewerRepository.RestrictedToMinimumLevel() }, }; return configuredLogLevels.AsReadOnly(); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 7f9fce6e4600..4006c7ca1642 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -17,6 +17,68 @@ namespace Umbraco.Extensions { public static class LoggerConfigExtensions { + /// + /// This configures Serilog with some defaults + /// 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. Scheduled for removal from Umbraco 13.")] + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) + { + return MinimalConfiguration(logConfig, hostingEnvironment, loggingConfiguration, configuration, out _); + } + + /// + /// This configures Serilog with some defaults + /// 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. Scheduled for removal from Umbraco 13.")] + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbFileConfiguration) + { + Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); + + //Set this environment variable - so that it can be used in external config file + //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> + Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.MapPathContentRoot("/").TrimEnd(Path.DirectorySeparatorChar), EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); + + logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) + .Enrich.WithProcessId() + .Enrich.WithProcessName() + .Enrich.WithThreadId() + .Enrich.WithProperty("ApplicationId", hostingEnvironment.ApplicationId) // Updated later by ApplicationIdEnricher + .Enrich.WithProperty("MachineName", Environment.MachineName) + .Enrich.With() + .Enrich.FromLogContext(); // allows us to dynamically enrich + + //This is not optimal, but seems to be the only way if we do not make an Serilog.Sink.UmbracoFile sink all the way. + var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); + + umbFileConfiguration = umbracoFileConfiguration; + + logConfig.WriteTo.UmbracoFile( + path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), + fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, + restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel, + rollingInterval: umbracoFileConfiguration.RollingInterval, + flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, + rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit); + + return logConfig; + } + /// /// This configures Serilog with some defaults /// Such as adding ProcessID, Thread, AppDomain etc @@ -60,6 +122,32 @@ public static LoggerConfiguration MinimalConfiguration( return logConfig; } + /// + /// Outputs a .txt format log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// + /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// + [Obsolete("Scheduled for removal from Umbraco 13.")] + public static LoggerConfiguration OutputDefaultTextFile( + this LoggerConfiguration logConfig, + Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, + LogEventLevel minimumLevel = LogEventLevel.Verbose) + { + //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"), + shared: true, + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: minimumLevel, + retainedFileCountLimit: null, //Setting to null means we keep all files - default is 31 days + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}"); + + return logConfig; + } + /// /// Outputs a .txt format log at /App_Data/Logs/ /// @@ -127,6 +215,35 @@ public static LoggerConfiguration UmbracoFile( null)); } + /// + /// Outputs a CLEF format JSON log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// The logging configuration + /// 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("Scheduled for removal from Umbraco 13.")] + public static LoggerConfiguration OutputDefaultJsonFile( + this LoggerConfiguration logConfig, + Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + LogEventLevel minimumLevel = LogEventLevel.Verbose, + int? retainedFileCount = null) + { + // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) + // 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"), + 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 + restrictedToMinimumLevel: minimumLevel); + + return logConfig; + } + /// /// Outputs a CLEF format JSON log at /App_Data/Logs/ /// diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index 565e20de39a3..cd485e766071 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Logging.Viewer; -[Obsolete("Use ILogViewerService instead. Scheduled removal in v18")] +[Obsolete("Use ILogViewerService instead. Scheduled removal in Umbraco 18.")] public abstract class SerilogLogViewerSourceBase : ILogViewer { private readonly ILogLevelLoader _logLevelLoader; From f7c30c9177a747a5894a94e01059a0387da842c9 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 1 Aug 2025 08:11:59 +0200 Subject: [PATCH 10/13] Expose repository from the service base class. --- .../Services/LogViewerServiceBase.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/LogViewerServiceBase.cs b/src/Umbraco.Core/Services/LogViewerServiceBase.cs index fbbae29dc6ba..0b69e964e17d 100644 --- a/src/Umbraco.Core/Services/LogViewerServiceBase.cs +++ b/src/Umbraco.Core/Services/LogViewerServiceBase.cs @@ -14,7 +14,6 @@ namespace Umbraco.Cms.Core.Services; /// public abstract class LogViewerServiceBase : ILogViewerService { - private readonly ILogViewerRepository _logViewerRepository; private readonly ILogViewerQueryRepository _logViewerQueryRepository; private readonly ICoreScopeProvider _provider; @@ -28,9 +27,14 @@ protected LogViewerServiceBase( { _logViewerQueryRepository = logViewerQueryRepository; _provider = provider; - _logViewerRepository = logViewerRepository; + LogViewerRepository = logViewerRepository; } + /// + /// Gets the . + /// + protected ILogViewerRepository LogViewerRepository { get; } + /// /// Gets the name of the logger. /// @@ -42,14 +46,14 @@ public virtual ReadOnlyDictionary GetLogLevelsFromSinks() var configuredLogLevels = new Dictionary { { "Global", GetGlobalMinLogLevel() }, - { LoggerName, _logViewerRepository.RestrictedToMinimumLevel() }, + { LoggerName, LogViewerRepository.RestrictedToMinimumLevel() }, }; return configuredLogLevels.AsReadOnly(); } /// - public virtual LogLevel GetGlobalMinLogLevel() => _logViewerRepository.GetGlobalMinLogLevel(); + public virtual LogLevel GetGlobalMinLogLevel() => LogViewerRepository.GetGlobalMinLogLevel(); /// public virtual Task GetSavedLogQueryByNameAsync(string name) @@ -123,7 +127,7 @@ public virtual Task> GetSavedLogQueriesAsync(int ski null); } - LogLevelCounts counter = _logViewerRepository.GetLogCount(logTimePeriod); + LogLevelCounts counter = LogViewerRepository.GetLogCount(logTimePeriod); return Attempt.SucceedWithStatus( LogViewerOperationStatus.Success, @@ -146,7 +150,7 @@ public virtual async Task, LogViewerOperationSta null!); } - LogTemplate[] messageTemplates = _logViewerRepository.GetMessageTemplates(logTimePeriod); + LogTemplate[] messageTemplates = LogViewerRepository.GetMessageTemplates(logTimePeriod); return Attempt.SucceedWithStatus( LogViewerOperationStatus.Success, @@ -224,7 +228,7 @@ protected PagedModel GetFilteredLogs( int skip, int take) { - IEnumerable logs = _logViewerRepository.GetLogs(logTimePeriod, filterExpression).ToArray(); + 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 From 46444ae3d4934fe0f3b8765e883a90098d655de5 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 1 Aug 2025 08:24:27 +0200 Subject: [PATCH 11/13] Restored further obsoleted code we can't remove yet. --- .../Logging/Serilog/SerilogLogger.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index da8ed26e3625..2b91932f8b80 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -19,8 +19,33 @@ public SerilogLogger(LoggerConfiguration logConfig) => public ILogger SerilogLog { get; } + [Obsolete("Scheduled for removal in Umbraco 17.")] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) => + CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); + public void Dispose() => SerilogLog.DisposeIfDisposable(); + /// + /// Creates a logger with some pre-defined configuration and remainder from config file + /// + /// Used by UmbracoApplicationBase to get its logger. + [Obsolete("Scheduled for removal in Umbraco 17.")] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbracoFileConfig) + { + LoggerConfiguration? serilogConfig = new LoggerConfiguration() + .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) + .ReadFrom.Configuration(configuration); + + return new SerilogLogger(serilogConfig); + } + public bool IsEnabled(Type reporting, LogLevel level) => LoggerFor(reporting).IsEnabled(MapLevel(level)); From 498d27c4fc22e163434b9015e8197fbd7b0f31d8 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 1 Aug 2025 08:54:05 +0200 Subject: [PATCH 12/13] Removed log viewer tests on removed class. We have integration tests for the new service. --- .../Logging/LogviewerTests.cs | 277 ------------------ 1 file changed, 277 deletions(-) delete mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs 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); -} From 0b6745885fe370cb963b9ee8eccc6b0fe7adcf96 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 1 Aug 2025 08:56:44 +0200 Subject: [PATCH 13/13] Obsoleted ILogViewer interface. --- .../DependencyInjection/UmbracoBuilder.Uniques.cs | 3 +++ src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs | 1 + 2 files changed, 4 insertions(+) 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/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; }