diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/AllLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/AllLogViewerController.cs new file mode 100644 index 0000000000..bb7d95957b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/AllLogViewerController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Logging.Viewer; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.New.Cms.Core.Models; +using LogLevel = Umbraco.Cms.Core.Logging.LogLevel; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer; + +public class AllLogViewerController : LogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllLogViewerController(ILogViewerService logViewerService, IUmbracoMapper umbracoMapper) + { + _logViewerService = logViewerService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated list of all logs for a specific date range. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// + /// By default this will be ordered descending (newest items first). + /// + /// The query expression to filter on (can be null). + /// The log levels for which to retrieve the log messages (can be null). + /// The start date for the date range (can be null). + /// The end date for the date range (can be null). + /// The paged result of the logs from the given time period. + [HttpGet("log")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task AllLogs( + int skip = 0, + int take = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + [FromQuery(Name = "logLevel")] LogLevel[]? logLevels = null, + DateTime? startDate = null, + DateTime? endDate = null) + { + var levels = logLevels?.Select(l => l.ToString()).ToArray(); + + Attempt?, LogViewerOperationStatus> logsAttempt = + await _logViewerService.GetPagedLogsAsync(startDate, endDate, skip, take, orderDirection, filterExpression, levels); + + if (logsAttempt.Success) + { + return Ok(_umbracoMapper.Map>(logsAttempt.Result)); + } + + return LogViewerOperationStatusResult(logsAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/AllSinkLevelLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/AllSinkLevelLogViewerController.cs new file mode 100644 index 0000000000..9c4ec553f1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/AllSinkLevelLogViewerController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer; + +public class AllSinkLevelLogViewerController : LogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllSinkLevelLogViewerController(ILogViewerService logViewerService, IUmbracoMapper umbracoMapper) + { + _logViewerService = logViewerService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated list of all loggers' levels. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// The paged result of the configured loggers and their level. + [HttpGet("level")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> AllLogLevels(int skip = 0, int take = 100) + { + IEnumerable> logLevels = _logViewerService + .GetLogLevelsFromSinks() + .Skip(skip) + .Take(take); + + return await Task.FromResult(Ok(_umbracoMapper.Map>(logLevels))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs new file mode 100644 index 0000000000..145bcd1eb8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Logging.Viewer; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer; + +public class LogLevelCountLogViewerController : LogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + private readonly IUmbracoMapper _umbracoMapper; + + public LogLevelCountLogViewerController(ILogViewerService logViewerService, IUmbracoMapper umbracoMapper) + { + _logViewerService = logViewerService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets the count for each log level from the logs for a specific date range. + /// + /// The start date for the date range (can be null). + /// The end date for the date range (can be null). + /// The log level counts from the (filtered) logs. + [HttpGet("level-count")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task LogLevelCounts(DateTime? startDate = null, DateTime? endDate = null) + { + Attempt logLevelCountsAttempt = + await _logViewerService.GetLogLevelCountsAsync(startDate, endDate); + + if (logLevelCountsAttempt.Success) + { + return Ok(_umbracoMapper.Map(logLevelCountsAttempt.Result)); + } + + return LogViewerOperationStatusResult(logLevelCountsAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogViewerControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogViewerControllerBase.cs new file mode 100644 index 0000000000..080d414442 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogViewerControllerBase.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer; + +[ApiController] +[VersionedApiBackOfficeRoute("log-viewer")] +[ApiExplorerSettings(GroupName = "Log Viewer")] +[ApiVersion("1.0")] +public abstract class LogViewerControllerBase : ManagementApiControllerBase +{ + protected IActionResult LogViewerOperationStatusResult(LogViewerOperationStatus status) => + status switch + { + LogViewerOperationStatus.NotFoundLogSearch => NotFound("The log search could not be found"), + LogViewerOperationStatus.DuplicateLogSearch => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Duplicate log search name") + .WithDetail("Another log search already exists with the attempted name.") + .Build()), + LogViewerOperationStatus.CancelledByLogsSizeValidation => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Cancelled due to log file size") + .WithDetail("The log file size for the requested date range prevented the operation.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown log viewer operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/MessageTemplateLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/MessageTemplateLogViewerController.cs new file mode 100644 index 0000000000..945e4985e1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/MessageTemplateLogViewerController.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core.Logging.Viewer; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer; + +public class MessageTemplateLogViewerController : LogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + private readonly IUmbracoMapper _umbracoMapper; + + public MessageTemplateLogViewerController(ILogViewerService logViewerService, IUmbracoMapper umbracoMapper) + { + _logViewerService = logViewerService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated list of all log message templates for a specific date range. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// The start date for the date range (can be null). + /// The end date for the date range (can be null). + /// The paged result of the log message templates from the given time period. + [HttpGet("message-template")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task AllMessageTemplates( + int skip = 0, + int take = 100, + DateTime? startDate = null, + DateTime? endDate = null) + { + Attempt, LogViewerOperationStatus> messageTemplatesAttempt = await _logViewerService.GetMessageTemplatesAsync(startDate, endDate); + + if (messageTemplatesAttempt.Success) + { + IEnumerable messageTemplates = messageTemplatesAttempt + .Result + .Skip(skip) + .Take(take); + + return Ok(_umbracoMapper.Map>(messageTemplates)); + } + + return LogViewerOperationStatusResult(messageTemplatesAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/AllSavedSearchLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/AllSavedSearchLogViewerController.cs new file mode 100644 index 0000000000..8f17403d9b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/AllSavedSearchLogViewerController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer.SavedSearch; + +public class AllSavedSearchLogViewerController : SavedSearchLogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllSavedSearchLogViewerController(ILogViewerService logViewerService, IUmbracoMapper umbracoMapper) + { + _logViewerService = logViewerService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated list of all saved log searches. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// The paged result of the saved log searches. + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> AllSavedSearches(int skip = 0, int take = 100) + { + IReadOnlyList savedLogQueries = await _logViewerService.GetSavedLogQueriesAsync(); + + return Ok(_umbracoMapper.Map>(savedLogQueries.Skip(skip).Take(take))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/ByNameSavedSearchLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/ByNameSavedSearchLogViewerController.cs new file mode 100644 index 0000000000..ef6e062423 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/ByNameSavedSearchLogViewerController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer.SavedSearch; + +public class ByNameSavedSearchLogViewerController : SavedSearchLogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByNameSavedSearchLogViewerController(ILogViewerService logViewerService, IUmbracoMapper umbracoMapper) + { + _logViewerService = logViewerService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a saved log search by name. + /// + /// The name of the saved log search. + /// The saved log search or not found result. + [HttpGet("{name}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(SavedLogSearchViewModel), StatusCodes.Status200OK)] + public async Task> ByName(string name) + { + ILogViewerQuery? savedLogQuery = await _logViewerService.GetSavedLogQueryByNameAsync(name); + + if (savedLogQuery is null) + { + return NotFound(); + } + + return Ok(_umbracoMapper.Map(savedLogQuery)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/CreateSavedSearchLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/CreateSavedSearchLogViewerController.cs new file mode 100644 index 0000000000..f7f77f2270 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/CreateSavedSearchLogViewerController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer.SavedSearch; + +public class CreateSavedSearchLogViewerController : SavedSearchLogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + + public CreateSavedSearchLogViewerController(ILogViewerService logViewerService) => _logViewerService = logViewerService; + + /// + /// Creates a saved log search. + /// + /// The log search to be saved. + /// The location of the saved log search after the creation. + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task Create(SavedLogSearchViewModel savedSearch) + { + Attempt result = + await _logViewerService.AddSavedLogQueryAsync(savedSearch.Name, savedSearch.Query); + + if (result.Success) + { + return CreatedAtAction( + controller => nameof(controller.ByName), savedSearch.Name); + } + + return LogViewerOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/DeleteSavedSearchLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/DeleteSavedSearchLogViewerController.cs new file mode 100644 index 0000000000..7ffe8daa74 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/DeleteSavedSearchLogViewerController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer.SavedSearch; + +public class DeleteSavedSearchLogViewerController : SavedSearchLogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + + public DeleteSavedSearchLogViewerController(ILogViewerService logViewerService) => _logViewerService = logViewerService; + + /// + /// Deletes a saved log search with a given name. + /// + /// The name of the saved log search. + /// The result of the deletion. + [HttpDelete("{name}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Delete(string name) + { + Attempt result = await _logViewerService.DeleteSavedLogQueryAsync(name); + + if (result.Success) + { + return Ok(); + } + + return LogViewerOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/SavedSearchLogViewerControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/SavedSearchLogViewerControllerBase.cs new file mode 100644 index 0000000000..5952f58d63 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/SavedSearch/SavedSearchLogViewerControllerBase.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer.SavedSearch; + +[ApiController] +[VersionedApiBackOfficeRoute("log-viewer/saved-search")] +[ApiExplorerSettings(GroupName = "Log Viewer")] +[ApiVersion("1.0")] +public class SavedSearchLogViewerControllerBase : LogViewerControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/ValidateLogFileSizeLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/ValidateLogFileSizeLogViewerController.cs new file mode 100644 index 0000000000..3950dd7c15 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/ValidateLogFileSizeLogViewerController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.LogViewer; + +public class ValidateLogFileSizeLogViewerController : LogViewerControllerBase +{ + private readonly ILogViewerService _logViewerService; + + public ValidateLogFileSizeLogViewerController(ILogViewerService logViewerService) => _logViewerService = logViewerService; + + /// + /// Gets a value indicating whether or not you are able to view logs for a specified date range. + /// + /// The start date for the date range (can be null). + /// The end date for the date range (can be null). + /// The boolean result. + [HttpGet("validate-logs-size")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task CanViewLogs(DateTime? startDate = null, DateTime? endDate = null) + { + Attempt result = await _logViewerService.CanViewLogsAsync(startDate, endDate); + + if (result.Success) + { + return Ok(); + } + + return LogViewerOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs index bf31c656dd..45e5ce4b22 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Filters; using Umbraco.Cms.Core.Security; @@ -22,6 +22,19 @@ public class ManagementApiControllerBase : Controller return base.CreatedAtAction(actionName, controllerName, new { key = key }, null); } + protected CreatedAtActionResult CreatedAtAction(Expression> action, string name) + { + if (action.Body is not ConstantExpression constantExpression) + { + throw new ArgumentException("Expression must be a constant expression."); + } + + var controllerName = ManagementApiRegexes.ControllerTypeToNameRegex().Replace(typeof(T).Name, string.Empty); + var actionName = constantExpression.Value?.ToString() ?? throw new ArgumentException("Expression does not have a value."); + + return base.CreatedAtAction(actionName, controllerName, new { name = name }, null); + } + protected static int CurrentUserId(IBackOfficeSecurityAccessor backOfficeSecurityAccessor) => backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? Core.Constants.Security.SuperUserId; } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/LogViewerBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/LogViewerBuilderExtensions.cs new file mode 100644 index 0000000000..beb3f0fc7a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/LogViewerBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Api.Management.Mapping.LogViewer; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class LogViewerBuilderExtensions +{ + internal static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index c5e94fa4c5..50790d8556 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -36,6 +36,7 @@ public class ManagementApiComposer : IComposer .AddTrackedReferences() .AddDataTypes() .AddTemplates() + .AddLogViewer() .AddBackOfficeAuthentication() .AddApiVersioning() .AddSwaggerGen(); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/LogViewer/LogViewerViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/LogViewer/LogViewerViewModelMapDefinition.cs new file mode 100644 index 0000000000..78321ead1e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/LogViewer/LogViewerViewModelMapDefinition.cs @@ -0,0 +1,111 @@ +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.LogViewer; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Logging.Viewer; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Mapping.LogViewer; + +public class LogViewerViewModelMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new LogLevelCountsViewModel(), Map); + mapper.Define, LogMessagePropertyViewModel>( + (source, context) => new LogMessagePropertyViewModel() { Name = string.Empty }, Map); + mapper.Define, LoggerViewModel>((source, context) => new LoggerViewModel() { Name = string.Empty }, Map); + mapper.Define( + (source, context) => new SavedLogSearchViewModel() + { + Name = string.Empty, + Query = string.Empty + }, + Map); + mapper.Define((source, context) => new LogTemplateViewModel(), Map); + mapper.Define((source, context) => new LogMessageViewModel(), Map); + mapper.Define>, PagedViewModel>((source, context) => new PagedViewModel(), Map); + mapper.Define, PagedViewModel>((source, context) => new PagedViewModel(), Map); + mapper.Define, PagedViewModel>((source, context) => new PagedViewModel(), Map); + mapper.Define, PagedViewModel>((source, context) => new PagedViewModel(), Map); + } + + // Umbraco.Code.MapAll + private static void Map(LogLevelCounts source, LogLevelCountsViewModel target, MapperContext context) + { + target.Information = source.Information; + target.Debug = source.Debug; + target.Warning = source.Warning; + target.Error = source.Error; + target.Fatal = source.Fatal; + } + + // Umbraco.Code.MapAll + private static void Map(KeyValuePair source, LogMessagePropertyViewModel target, MapperContext context) + { + target.Name = source.Key; + target.Value = source.Value; + } + + // Umbraco.Code.MapAll + private static void Map(KeyValuePair source, LoggerViewModel target, MapperContext context) + { + target.Name = source.Key; + target.Level = source.Value; + } + + // Umbraco.Code.MapAll + private static void Map(ILogViewerQuery source, SavedLogSearchViewModel target, MapperContext context) + { + target.Name = source.Name; + target.Query = source.Query; + } + + // Umbraco.Code.MapAll + private static void Map(LogTemplate source, LogTemplateViewModel target, MapperContext context) + { + target.MessageTemplate = source.MessageTemplate; + target.Count = source.Count; + } + + + // Umbraco.Code.MapAll + private static void Map(ILogEntry source, LogMessageViewModel target, MapperContext context) + { + target.Timestamp = source.Timestamp; + target.Level = source.Level; + target.MessageTemplate = source.MessageTemplateText; + target.RenderedMessage = source.RenderedMessage; + target.Properties = context.MapEnumerable, LogMessagePropertyViewModel>(source.Properties); + target.Exception = source.Exception; + } + + // Umbraco.Code.MapAll + private static void Map(IEnumerable> source, PagedViewModel target, MapperContext context) + { + target.Items = context.MapEnumerable, LoggerViewModel>(source); + target.Total = source.Count(); + } + + // Umbraco.Code.MapAll + private static void Map(IEnumerable source, PagedViewModel target, MapperContext context) + { + target.Items = context.MapEnumerable(source); + target.Total = source.Count(); + } + + // Umbraco.Code.MapAll + private static void Map(IEnumerable source, PagedViewModel target, MapperContext context) + { + target.Items = context.MapEnumerable(source); + target.Total = source.Count(); + } + + // Umbraco.Code.MapAll + private static void Map(PagedModel source, PagedViewModel target, MapperContext context) + { + target.Items = context.MapEnumerable(source.Items); + target.Total = source.Total; + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 166849c65e..36a567fd55 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -638,6 +638,48 @@ } } }, + "delete": { + "tags": [ + "Dictionary" + ], + "operationId": "DeleteDictionaryByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, "put": { "tags": [ "Dictionary" @@ -688,38 +730,6 @@ } } } - }, - "delete": { - "tags": [ - "Dictionary" - ], - "operationId": "DeleteDictionaryByKey", - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Success" - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } } }, "/umbraco/management/api/v1/dictionary/export/{key}": { @@ -826,7 +836,7 @@ ], "operationId": "PostDictionaryUpload", "requestBody": { - "content": { } + "content": {} }, "responses": { "200": { @@ -2092,6 +2102,415 @@ } } }, + "/umbraco/management/api/v1/log-viewer/level": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerLevel", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedLogger" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/log-viewer/level-count": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerLevelCount", + "parameters": [ + { + "name": "startDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "endDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/log-viewer/log": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerLog", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + }, + { + "name": "orderDirection", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Direction" + } + }, + { + "name": "filterExpression", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "logLevel", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogLevel" + } + } + }, + { + "name": "startDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "endDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedLogMessage" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/log-viewer/message-template": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerMessageTemplate", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + }, + { + "name": "startDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "endDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedLogTemplate" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/log-viewer/saved-search": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerSavedSearch", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedSavedLogSearch" + } + } + } + } + } + }, + "post": { + "tags": [ + "Log Viewer" + ], + "operationId": "PostLogViewerSavedSearch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SavedLogSearch" + } + } + } + }, + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "201": { + "description": "Created" + } + } + } + }, + "/umbraco/management/api/v1/log-viewer/saved-search/{name}": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerSavedSearchByName", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResult" + } + } + } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SavedLogSearch" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Log Viewer" + ], + "operationId": "DeleteLogViewerSavedSearchByName", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResult" + } + } + } + }, + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/log-viewer/validate-logs-size": { + "get": { + "tags": [ + "Log Viewer" + ], + "operationId": "GetLogViewerValidateLogsSize", + "parameters": [ + { + "name": "startDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "endDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "Success" + } + } + } + }, "/umbraco/management/api/v1/tree/media-type/children": { "get": { "tags": [ @@ -5216,6 +5635,14 @@ }, "additionalProperties": false }, + "Direction": { + "enum": [ + "Ascending", + "Descending" + ], + "type": "integer", + "format": "int32" + }, "DocumentBlueprintTreeItem": { "type": "object", "properties": { @@ -5902,7 +6329,7 @@ }, "providerProperties": { "type": "object", - "additionalProperties": { }, + "additionalProperties": {}, "nullable": true } }, @@ -5987,6 +6414,88 @@ "type": "integer", "format": "int32" }, + "LogLevel": { + "enum": [ + "Verbose", + "Debug", + "Information", + "Warning", + "Error", + "Fatal" + ], + "type": "integer", + "format": "int32" + }, + "LogMessage": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "level": { + "$ref": "#/components/schemas/LogLevel" + }, + "messageTemplate": { + "type": "string", + "nullable": true + }, + "renderedMessage": { + "type": "string", + "nullable": true + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogMessageProperty" + } + }, + "exception": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LogMessageProperty": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LogTemplate": { + "type": "object", + "properties": { + "messageTemplate": { + "type": "string", + "nullable": true + }, + "count": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "Logger": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "$ref": "#/components/schemas/LogLevel" + } + }, + "additionalProperties": false + }, "MemberInfo": { "type": "object", "properties": { @@ -6771,6 +7280,66 @@ }, "additionalProperties": false }, + "PagedLogMessage": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogMessage" + } + } + }, + "additionalProperties": false + }, + "PagedLogTemplate": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogTemplate" + } + } + }, + "additionalProperties": false + }, + "PagedLogger": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Logger" + } + } + }, + "additionalProperties": false + }, "PagedRecycleBinItem": { "required": [ "items", @@ -6851,6 +7420,26 @@ }, "additionalProperties": false }, + "PagedSavedLogSearch": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SavedLogSearch" + } + } + }, + "additionalProperties": false + }, "PagedSearchResult": { "required": [ "items", @@ -7022,7 +7611,7 @@ "nullable": true } }, - "additionalProperties": { } + "additionalProperties": {} }, "ProfilingStatus": { "type": "object", @@ -7296,6 +7885,18 @@ }, "additionalProperties": false }, + "SavedLogSearch": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "query": { + "type": "string" + } + }, + "additionalProperties": false + }, "SearchResult": { "type": "object", "properties": { @@ -8368,7 +8969,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token", - "scopes": { } + "scopes": {} } } } @@ -8376,7 +8977,7 @@ }, "security": [ { - "OAuth": [ ] + "OAuth": [] } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogLevelCountsViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogLevelCountsViewModel.cs new file mode 100644 index 0000000000..b3e564767a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogLevelCountsViewModel.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.LogViewer; + +public class LogLevelCountsViewModel +{ + /// + /// Gets or sets the Information level count. + /// + public int Information { get; set; } + + /// + /// Gets or sets the Debug level count. + /// + public int Debug { get; set; } + + /// + /// Gets or sets the Warning level count. + /// + public int Warning { get; set; } + + /// + /// Gets or sets the Error level count. + /// + public int Error { get; set; } + + /// + /// Gets or sets the Fatal level count. + /// + public int Fatal { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogMessagePropertyViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogMessagePropertyViewModel.cs new file mode 100644 index 0000000000..a029f0aa5c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogMessagePropertyViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.LogViewer; + +public class LogMessagePropertyViewModel +{ + /// + /// Gets or sets the name of the log message property. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the value of the log message property (can be null). + /// + public string? Value { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogMessageViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogMessageViewModel.cs new file mode 100644 index 0000000000..684a1492d7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogMessageViewModel.cs @@ -0,0 +1,36 @@ +using LogLevel = Umbraco.Cms.Core.Logging.LogLevel; + +namespace Umbraco.Cms.Api.Management.ViewModels.LogViewer; + +public class LogMessageViewModel +{ + /// + /// Gets or sets the time at which the log event occurred. + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Gets or sets the level of the event. + /// + public LogLevel Level { get; set; } + + /// + /// Gets or sets the message template describing the log event (can be null). + /// + public string? MessageTemplate { get; set; } + + /// + /// Gets or sets the message template filled with the log event properties (can be null). + /// + public string? RenderedMessage { get; set; } + + /// + /// Gets or sets the properties associated with the log event, including those presented in MessageTemplate. + /// + public IEnumerable Properties { get; set; } = null!; + + /// + /// Gets or sets an exception associated with the log event (can be null). + /// + public string? Exception { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogTemplateViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogTemplateViewModel.cs new file mode 100644 index 0000000000..e4e9b55cac --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LogTemplateViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.LogViewer; + +public class LogTemplateViewModel +{ + /// + /// Gets or sets the message template. + /// + public string? MessageTemplate { get; set; } + + /// + /// Gets or sets the count. + /// + public int Count { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LoggerViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LoggerViewModel.cs new file mode 100644 index 0000000000..51df0036ec --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/LoggerViewModel.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Logging; + +namespace Umbraco.Cms.Api.Management.ViewModels.LogViewer; + +public class LoggerViewModel +{ + /// + /// Gets or sets the name of the log event sink. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the log event level (can be null). + /// + public LogLevel Level { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/SavedLogSearchViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/SavedLogSearchViewModel.cs new file mode 100644 index 0000000000..e39c0cf1ff --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/LogViewer/SavedLogSearchViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.LogViewer; + +public class SavedLogSearchViewModel +{ + /// + /// Gets or sets the name of the saved search. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the query of the saved search. + /// + public required string Query { get; set; } +} diff --git a/src/Umbraco.Core/Logging/Viewer/ILogEntry.cs b/src/Umbraco.Core/Logging/Viewer/ILogEntry.cs new file mode 100644 index 0000000000..c690b70f28 --- /dev/null +++ b/src/Umbraco.Core/Logging/Viewer/ILogEntry.cs @@ -0,0 +1,34 @@ +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogEntry +{ + /// + /// Gets or sets the time at which the log event occurred. + /// + DateTimeOffset Timestamp { get; set; } + + /// + /// Gets or sets the level of the log event. + /// + LogLevel Level { get; set; } + + /// + /// Gets or sets the message template describing the log event. + /// + string? MessageTemplateText { get; set; } + + /// + /// Gets or sets the message template filled with the log event properties. + /// + string? RenderedMessage { get; set; } + + /// + /// Gets or sets the properties associated with the log event. + /// + IReadOnlyDictionary Properties { get; set; } + + /// + /// Gets or sets an exception associated with the log event, or null. + /// + string? Exception { get; set; } +} diff --git a/src/Umbraco.Core/Logging/Viewer/LogEntry.cs b/src/Umbraco.Core/Logging/Viewer/LogEntry.cs new file mode 100644 index 0000000000..b5c54eddbe --- /dev/null +++ b/src/Umbraco.Core/Logging/Viewer/LogEntry.cs @@ -0,0 +1,34 @@ +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogEntry : ILogEntry +{ + /// + /// Gets or sets the time at which the log event occurred. + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Gets or sets the level of the log event. + /// + public LogLevel Level { get; set; } + + /// + /// Gets or sets the message template describing the log event. + /// + public string? MessageTemplateText { get; set; } + + /// + /// Gets or sets the message template filled with the log event properties. + /// + public string? RenderedMessage { get; set; } + + /// + /// Gets or sets the properties associated with the log event. + /// + public IReadOnlyDictionary Properties { get; set; } = new Dictionary(); + + /// + /// Gets or sets an exception associated with the log event, or null. + /// + public string? Exception { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs b/src/Umbraco.Core/Logging/Viewer/LogLevelCounts.cs similarity index 100% rename from src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs rename to src/Umbraco.Core/Logging/Viewer/LogLevelCounts.cs diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs b/src/Umbraco.Core/Logging/Viewer/LogTemplate.cs similarity index 100% rename from src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs rename to src/Umbraco.Core/Logging/Viewer/LogTemplate.cs diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs b/src/Umbraco.Core/Logging/Viewer/LogTimePeriod.cs similarity index 100% rename from src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs rename to src/Umbraco.Core/Logging/Viewer/LogTimePeriod.cs diff --git a/src/Umbraco.Core/Services/ILogViewerService.cs b/src/Umbraco.Core/Services/ILogViewerService.cs new file mode 100644 index 0000000000..8df8e11718 --- /dev/null +++ b/src/Umbraco.Core/Services/ILogViewerService.cs @@ -0,0 +1,94 @@ +using System.Collections.ObjectModel; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Logging.Viewer; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface ILogViewerService : IService +{ + /// + /// Gets all logs as a paged model. The attempt will fail if the log files + /// for the given time period are too large (more than 1GB). + /// + /// The start date for the date range. + /// The end date for the date range. + /// The amount of items to skip. + /// The amount of items to take. + /// + /// The direction in which the log entries are to be ordered. + /// + /// The query expression to filter on. + /// The log levels for which to retrieve the log messages. + Task?, LogViewerOperationStatus>> GetPagedLogsAsync( + DateTime? startDate, + DateTime? endDate, + int skip, + int take, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null); + + /// + /// Get all saved log queries from your chosen data source. + /// + Task> GetSavedLogQueriesAsync(); + + /// + /// Gets a saved log query by name from your chosen data source. + /// + /// The name of the saved log query. + Task GetSavedLogQueryByNameAsync(string name); + + /// + /// Adds a new saved log query to your chosen data source. + /// + /// The name of the new saved log query. + /// The query of the new saved log query. + Task> AddSavedLogQueryAsync(string name, string query); + + /// + /// Deletes a saved log query to your chosen data source. + /// + /// The name of the saved log search. + Task> DeleteSavedLogQueryAsync(string name); + + /// + /// Returns a value indicating whether the log files for the given time + /// period are not too large to view (more than 1GB). + /// + /// The start date for the date range. + /// The end date for the date range. + /// The value whether or not you are able to view the logs. + Task> CanViewLogsAsync(DateTime? startDate, DateTime? endDate); + + /// + /// Returns a number of the different log level entries. + /// The attempt will fail if the log files for the given + /// time period are too large (more than 1GB). + /// + /// The start date for the date range. + /// The end date for the date range. + Task> GetLogLevelCountsAsync(DateTime? startDate, DateTime? endDate); + + /// + /// Returns a list of all unique message templates and their counts. + /// The attempt will fail if the log files for the given + /// time period are too large (more than 1GB). + /// + /// The start date for the date range. + /// The end date for the date range. + Task, LogViewerOperationStatus>> GetMessageTemplatesAsync(DateTime? startDate, DateTime? endDate); + + /// + /// Get the log level values of the global minimum and the UmbracoFile one from the config file. + /// + ReadOnlyDictionary GetLogLevelsFromSinks(); + + /// + /// Get the minimum log level value from the config file. + /// + LogLevel GetGlobalMinLogLevel(); +} diff --git a/src/Umbraco.Core/Services/OperationStatus/LogViewerOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/LogViewerOperationStatus.cs new file mode 100644 index 0000000000..b929d80e04 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/LogViewerOperationStatus.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum LogViewerOperationStatus +{ + Success, + NotFoundLogSearch, + DuplicateLogSearch, + CancelledByLogsSizeValidation +} diff --git a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml index 6062ef76da..7eb0063a32 100644 --- a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml +++ b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml @@ -1,5 +1,26 @@  + + CP0001 + T:Umbraco.Cms.Core.Logging.Viewer.LogLevelCounts + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0001 + T:Umbraco.Cms.Core.Logging.Viewer.LogTemplate + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0001 + T:Umbraco.Cms.Core.Logging.Viewer.LogTimePeriod + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + CP0001 T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.ClearCsrfCookies @@ -126,6 +147,13 @@ lib/net7.0/Umbraco.Infrastructure.dll true + + CP0006 + M:Umbraco.Cms.Core.Logging.Viewer.ILogViewer.GetLogsAsPagedModel(Umbraco.Cms.Core.Logging.Viewer.LogTimePeriod,System.Int32,System.Int32,Umbraco.Cms.Core.Direction,System.String,System.String[]) + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + CP0006 M:Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor.ExecutePlan(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index d6c3e95c47..bcdb6c3eeb 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -32,6 +32,7 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Trees; @@ -232,6 +233,7 @@ public static partial class UmbracoBuilderExtensions factory.GetRequiredService(), factory.GetRequiredService(), Log.Logger)); + builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs index 25576b88da..6d36ef8701 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs @@ -3,6 +3,7 @@ using Serilog.Events; namespace Umbraco.Cms.Core.Logging.Viewer; +[Obsolete("Use ILogViewerService instead. Scheduled for removal in Umbraco 15.")] public interface ILogLevelLoader { /// diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 711906043a..9b12111486 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Logging.Viewer; @@ -9,16 +10,19 @@ public interface ILogViewer /// /// Get all saved searches from your chosen data source /// + [Obsolete("Use ILogViewerService.GetSavedLogQueriesAsync instead. Scheduled for removal in Umbraco 15.")] IReadOnlyList GetSavedSearches(); /// /// Adds a new saved search to chosen data source and returns the updated searches /// + [Obsolete("Use ILogViewerService.AddSavedLogQueryAsync instead. Scheduled for removal in Umbraco 15.")] IReadOnlyList AddSavedSearch(string name, string query); /// /// Deletes a saved search to chosen data source and returns the remaining searches /// + [Obsolete("Use ILogViewerService.DeleteSavedLogQueryAsync instead. Scheduled for removal in Umbraco 15.")] IReadOnlyList DeleteSavedSearch(string name) => DeleteSavedSearch(name, string.Empty); [Obsolete("Use the overload that only takes a 'name' parameter instead. This will be removed in Umbraco 14.")] @@ -33,18 +37,22 @@ public interface ILogViewer /// /// Returns a number of the different log level entries /// + [Obsolete("Use ILogViewerService.GetLogLevelCounts instead. Scheduled for removal in Umbraco 15.")] LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); /// /// Returns a list of all unique message templates and their counts /// + [Obsolete("Use ILogViewerService.GetMessageTemplates instead. Scheduled for removal in Umbraco 15.")] IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); + [Obsolete("Use ILogViewerService.CanViewLogsAsync instead. Scheduled for removal in Umbraco 15.")] bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); /// /// Returns the collection of logs /// + [Obsolete("Use ILogViewerService.GetPagedLogs instead. Scheduled for removal in Umbraco 15.")] PagedResult GetLogs( LogTimePeriod logTimePeriod, int pageNumber = 1, @@ -52,4 +60,13 @@ public interface ILogViewer Direction orderDirection = Direction.Descending, string? filterExpression = null, string[]? logLevels = null); + + [Obsolete("Use ILogViewerService.GetPagedLogs instead. Scheduled for removal in Umbraco 15.")] + PagedModel GetLogsAsPagedModel( + LogTimePeriod logTimePeriod, + int skip, + int take, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs index ed67c58b5d..31c016102c 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -1,5 +1,6 @@ namespace Umbraco.Cms.Core.Logging.Viewer; +[Obsolete("Use ILogViewerService instead. Scheduled for removal in Umbraco 15.")] public interface ILogViewerConfig { IReadOnlyList GetSavedSearches(); diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs index a0f6927ef7..ccac21f17f 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs @@ -14,6 +14,7 @@ public class LogLevelLoader : ILogLevelLoader /// /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. /// + [Obsolete("Use ILogViewerService.GetLogLevelsFromSinks instead. Scheduled for removal in Umbraco 15.")] public ReadOnlyDictionary GetLogLevelsFromSinks() { var configuredLogLevels = new Dictionary @@ -27,6 +28,7 @@ public class LogLevelLoader : ILogLevelLoader /// /// Get the Serilog minimum-level value from the config file. /// + [Obsolete("Use ILogViewerService.GetGlobalMinLogLevel instead. Scheduled for removal in Umbraco 15.")] public LogEventLevel? GetGlobalMinLogLevel() { LogEventLevel? logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled) diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs index 3974a8da1e..42ad881ea3 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs @@ -5,6 +5,7 @@ using Serilog.Events; // ReSharper disable UnusedAutoPropertyAccessor.Global namespace Umbraco.Cms.Core.Logging.Viewer; +[Obsolete("Use ILogEntry instead. Scheduled for removal in Umbraco 15.")] public class LogMessage { /// diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index ecd9aaf329..96be41d6fe 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -16,6 +16,7 @@ public class LogViewerConfig : ILogViewerConfig _scopeProvider = scopeProvider; } + [Obsolete("Use ILogViewerService.GetSavedLogQueriesAsync instead. Scheduled for removal in Umbraco 15.")] public IReadOnlyList GetSavedSearches() { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); @@ -24,6 +25,7 @@ public class LogViewerConfig : ILogViewerConfig return result; } + [Obsolete("Use ILogViewerService.AddSavedLogQueryAsync instead. Scheduled for removal in Umbraco 15.")] public IReadOnlyList AddSavedSearch(string name, string query) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); @@ -35,6 +37,7 @@ public class LogViewerConfig : ILogViewerConfig [Obsolete("Use the overload that only takes a 'name' parameter instead. This will be removed in Umbraco 14.")] public IReadOnlyList DeleteSavedSearch(string name, string query) => DeleteSavedSearch(name); + [Obsolete("Use ILogViewerService.DeleteSavedLogQueryAsync instead. Scheduled for removal in Umbraco 15.")] public IReadOnlyList DeleteSavedSearch(string name) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index c573f237b2..73d88ac8a8 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -26,6 +26,7 @@ internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase public override bool CanHandleLargeLogs => false; + [Obsolete("Use ILogViewerService.CanViewLogsAsync instead. Scheduled for removal in Umbraco 15.")] public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) { // Log Directory diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index e83597d216..f933da0e75 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -3,6 +3,7 @@ using Serilog; using Serilog.Events; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Logging.Viewer; @@ -19,11 +20,14 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer public abstract bool CanHandleLargeLogs { get; } + [Obsolete("Use ILogViewerService.CanViewLogsAsync instead. Scheduled for removal in Umbraco 15.")] public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + [Obsolete("Use ILogViewerService.GetSavedLogQueriesAsync instead. Scheduled for removal in Umbraco 15.")] public virtual IReadOnlyList GetSavedSearches() => _logViewerConfig.GetSavedSearches(); + [Obsolete("Use ILogViewerService.AddSavedLogQueryAsync instead. Scheduled for removal in Umbraco 15.")] public virtual IReadOnlyList AddSavedSearch(string name, string query) => _logViewerConfig.AddSavedSearch(name, query); @@ -31,6 +35,7 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer public virtual IReadOnlyList DeleteSavedSearch(string name, string query) => DeleteSavedSearch(name); + [Obsolete("Use ILogViewerService.DeleteSavedLogQueryAsync instead. Scheduled for removal in Umbraco 15.")] public virtual IReadOnlyList DeleteSavedSearch(string name) => _logViewerConfig.DeleteSavedSearch(name); @@ -41,6 +46,7 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer return errorCounter.Count; } + [Obsolete("Use ILogViewerService.GetLogLevelCounts instead. Scheduled for removal in Umbraco 15.")] public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) { var counter = new CountingFilter(); @@ -48,6 +54,7 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer return counter.Counts; } + [Obsolete("Use ILogViewerService.GetMessageTemplates instead. Scheduled for removal in Umbraco 15.")] public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) { var messageTemplates = new MessageTemplateFilter(); @@ -60,6 +67,7 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer return templates; } + [Obsolete("Use ILogViewerService.GetPagedLogs instead. Scheduled for removal in Umbraco 15.")] public PagedResult GetLogs( LogTimePeriod logTimePeriod, int pageNumber = 1, @@ -67,6 +75,72 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer Direction orderDirection = Direction.Descending, string? filterExpression = null, string[]? logLevels = null) + { + IReadOnlyList filteredLogs = GetFilteredLogs(logTimePeriod, filterExpression, logLevels); + long totalRecords = filteredLogs.Count; + + // Order By, Skip, Take & Select + IEnumerable logMessages = filteredLogs + .OrderBy(l => l.Timestamp, orderDirection) + .Skip(pageSize * (pageNumber - 1)) + .Take(pageSize) + .Select(x => new LogMessage + { + Timestamp = x.Timestamp, + Level = x.Level, + MessageTemplateText = x.MessageTemplate.Text, + Exception = x.Exception?.ToString(), + Properties = x.Properties, + RenderedMessage = x.RenderMessage(), + }); + + return new PagedResult(totalRecords, pageNumber, pageSize) { Items = logMessages }; + } + + [Obsolete("Use ILogViewerService.GetPagedLogs instead. Scheduled for removal in Umbraco 15.")] + public PagedModel GetLogsAsPagedModel( + LogTimePeriod logTimePeriod, + int skip, + int take, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null) + { + IReadOnlyList filteredLogs = GetFilteredLogs(logTimePeriod, filterExpression, logLevels); + + // Order By, Skip, Take & Select + IEnumerable logMessages = filteredLogs + .OrderBy(l => l.Timestamp, orderDirection) + .Skip(skip) + .Take(take) + .Select(x => new LogMessage + { + Timestamp = x.Timestamp, + Level = x.Level, + MessageTemplateText = x.MessageTemplate.Text, + Exception = x.Exception?.ToString(), + Properties = x.Properties, + RenderedMessage = x.RenderMessage(), + }); + + return new PagedModel(logMessages.Count(), logMessages); + } + + /// + /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. + /// + [Obsolete("Use ILogViewerService.GetLogLevelsFromSinks instead. Scheduled for removal in Umbraco 15.")] + public ReadOnlyDictionary GetLogLevels() => _logLevelLoader.GetLogLevelsFromSinks(); + + /// + /// Get all logs from your chosen data source back as Serilog LogEvents + /// + protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); + + private IReadOnlyList GetFilteredLogs( + LogTimePeriod logTimePeriod, + string? filterExpression, + string[]? logLevels) { var expression = new ExpressionFilter(filterExpression); IReadOnlyList filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); @@ -98,33 +172,6 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer } } - long totalRecords = filteredLogs.Count; - - // Order By, Skip, Take & Select - IEnumerable logMessages = filteredLogs - .OrderBy(l => l.Timestamp, orderDirection) - .Skip(pageSize * (pageNumber - 1)) - .Take(pageSize) - .Select(x => new LogMessage - { - Timestamp = x.Timestamp, - Level = x.Level, - MessageTemplateText = x.MessageTemplate.Text, - Exception = x.Exception?.ToString(), - Properties = x.Properties, - RenderedMessage = x.RenderMessage(), - }); - - return new PagedResult(totalRecords, pageNumber, pageSize) { Items = logMessages }; + return filteredLogs; } - - /// - /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. - /// - public ReadOnlyDictionary GetLogLevels() => _logLevelLoader.GetLogLevelsFromSinks(); - - /// - /// Get all logs from your chosen data source back as Serilog LogEvents - /// - protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/LogViewerService.cs b/src/Umbraco.Infrastructure/Services/Implement/LogViewerService.cs new file mode 100644 index 0000000000..4da777175c --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/LogViewerService.cs @@ -0,0 +1,260 @@ +using Serilog.Events; +using System.Collections.ObjectModel; +using System.Text.Json; +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 Umbraco.New.Cms.Core.Models; +using LogLevel = Umbraco.Cms.Core.Logging.LogLevel; + +namespace Umbraco.Cms.Core.Services.Implement; + +// FIXME: Get rid of ILogViewer and ILogLevelLoader dependencies (as they are obsolete) +// and fix the implementation of the methods using it +public class LogViewerService : ILogViewerService +{ + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly ILogViewer _logViewer; + private readonly ILogLevelLoader _logLevelLoader; + private readonly ICoreScopeProvider _provider; + + public LogViewerService( + ILogViewerQueryRepository logViewerQueryRepository, + ILogViewer logViewer, + ILogLevelLoader logLevelLoader, + ICoreScopeProvider provider) + { + _logViewerQueryRepository = logViewerQueryRepository; + _logViewer = logViewer; + _logLevelLoader = logLevelLoader; + _provider = provider; + } + + /// + public async Task?, LogViewerOperationStatus>> GetPagedLogsAsync( + DateTime? startDate, + DateTime? 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 Attempt.FailWithStatus?, LogViewerOperationStatus>( + LogViewerOperationStatus.CancelledByLogsSizeValidation, + null); + } + + PagedModel logMessages = + _logViewer.GetLogsAsPagedModel(logTimePeriod, skip, take, orderDirection, filterExpression, logLevels); + + var logEntries = new PagedModel(logMessages.Total, logMessages.Items.Select(x => ToLogEntry(x))); + + return Attempt.SucceedWithStatus?, LogViewerOperationStatus>( + LogViewerOperationStatus.Success, + logEntries); + } + + /// + public async Task> GetSavedLogQueriesAsync() + { + using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); + return await Task.FromResult(_logViewerQueryRepository.GetMany().ToList()); + } + + /// + public async Task GetSavedLogQueryByNameAsync(string name) + { + using ICoreScope scope = _provider.CreateCoreScope(autoComplete: true); + return await 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 async Task> CanViewLogsAsync(DateTime? startDate, DateTime? endDate) + { + LogTimePeriod logTimePeriod = GetTimePeriod(startDate, endDate); + bool isAllowed = CanViewLogs(logTimePeriod); + + if (isAllowed == false) + { + return Attempt.FailWithStatus(LogViewerOperationStatus.CancelledByLogsSizeValidation, isAllowed); + } + + return Attempt.SucceedWithStatus(LogViewerOperationStatus.Success, isAllowed); + } + + /// + public async Task> GetLogLevelCountsAsync(DateTime? startDate, DateTime? 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 Attempt.FailWithStatus( + LogViewerOperationStatus.CancelledByLogsSizeValidation, + null); + } + + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, + _logViewer.GetLogLevelCounts(logTimePeriod)); + } + + /// + public async Task, LogViewerOperationStatus>> GetMessageTemplatesAsync(DateTime? startDate, DateTime? 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 Attempt.FailWithStatus( + LogViewerOperationStatus.CancelledByLogsSizeValidation, + Enumerable.Empty()); + } + + return Attempt.SucceedWithStatus( + LogViewerOperationStatus.Success, + _logViewer.GetMessageTemplates(logTimePeriod)); + } + + /// + public ReadOnlyDictionary GetLogLevelsFromSinks() + { + ReadOnlyDictionary configuredLogLevels = _logLevelLoader.GetLogLevelsFromSinks(); + + return configuredLogLevels.ToDictionary(logLevel => logLevel.Key, logLevel => Enum.Parse(logLevel.Value!.ToString()!)).AsReadOnly(); + } + + /// + public LogLevel GetGlobalMinLogLevel() + { + LogEventLevel? serilogLogLevel = _logLevelLoader.GetGlobalMinLogLevel(); + + return Enum.Parse(serilogLogLevel!.ToString()!); + } + + /// + /// 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(DateTime? startDate, DateTime? 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, endDate.Value); + } + + /// + /// 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. + private bool CanViewLogs(LogTimePeriod logTimePeriod) + { + // Check if the interface can deal with large files + if (_logViewer.CanHandleLargeLogs) + { + return true; + } + + return _logViewer.CheckCanOpenLogs(logTimePeriod); + } + + private ILogEntry ToLogEntry(LogMessage logMessage) + { + return new LogEntry() + { + Timestamp = logMessage.Timestamp, + Level = Enum.Parse(logMessage.Level.ToString()), + MessageTemplateText = logMessage.MessageTemplateText, + RenderedMessage = logMessage.RenderedMessage, + Properties = MapLogMessageProperties(logMessage.Properties), + Exception = logMessage.Exception + }; + } + + private IReadOnlyDictionary MapLogMessageProperties(IReadOnlyDictionary? properties) + { + var result = new Dictionary(); + + if (properties is not null) + { + foreach (KeyValuePair property in properties) + { + string? value; + + if (property.Value is ScalarValue scalarValue) + { + value = scalarValue.Value?.ToString(); + } + else + { + // When polymorphism is implemented, this should be changed + value = JsonSerializer.Serialize(property.Value as object); + } + + result.Add(property.Key, value); + } + } + + return result.AsReadOnly(); + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 74615e1fa8..c59339c294 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Serilog.Events; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.Attributes; diff --git a/umbraco.sln b/umbraco.sln index 804983d0a7..f6150893c4 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -181,8 +181,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Imaging.ImageSh EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{05878304-40EB-4F84-B40B-91BDB70DE094}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI.New", "src\Umbraco.Web.UI.New\Umbraco.Web.UI.New.csproj", "{C55CA725-9F4E-4618-9435-6B8AE05DA14D}" -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Api.Common", "src\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj", "{D48B5D6B-82FF-4235-986C-CDE646F41DEC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.UI.New", "src\Umbraco.Web.UI.New\Umbraco.Web.UI.New.csproj", "{C55CA725-9F4E-4618-9435-6B8AE05DA14D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Api.Common", "src\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj", "{D48B5D6B-82FF-4235-986C-CDE646F41DEC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution