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