Merge branch temp8 into temp8-fixtree

This commit is contained in:
Stephan
2019-01-22 14:08:14 +01:00
77 changed files with 4431 additions and 168 deletions
+3 -1
View File
@@ -145,9 +145,11 @@
public const string PartialViewMacros = "partialViewMacros";
public const string LogViewer = "logViewer";
public static class Groups
{
public const string Settings = "settingsGroup";
public const string Settings = "settingsGroup";
public const string Templating = "templatingGroup";
+6
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core
{
@@ -337,5 +338,10 @@ namespace Umbraco.Core
yield return value;
}
}
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Direction sortOrder)
{
return sortOrder == Direction.Ascending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector);
}
}
}
@@ -0,0 +1,49 @@
using System;
using Serilog.Events;
namespace Umbraco.Core.Logging.Viewer
{
internal class CountingFilter : ILogFilter
{
public CountingFilter()
{
Counts = new LogLevelCounts();
}
public LogLevelCounts Counts { get; }
public bool TakeLogEvent(LogEvent e)
{
switch (e.Level)
{
case LogEventLevel.Debug:
Counts.Debug++;
break;
case LogEventLevel.Information:
Counts.Information++;
break;
case LogEventLevel.Warning:
Counts.Warning++;
break;
case LogEventLevel.Error:
Counts.Error++;
break;
case LogEventLevel.Fatal:
Counts.Fatal++;
break;
case LogEventLevel.Verbose:
break;
default:
throw new ArgumentOutOfRangeException();
}
//Don't add it to the list
return false;
}
}
}
@@ -0,0 +1,18 @@
using Serilog.Events;
namespace Umbraco.Core.Logging.Viewer
{
internal class ErrorCounterFilter : ILogFilter
{
public int Count { get; private set; }
public bool TakeLogEvent(LogEvent e)
{
if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null)
Count++;
//Don't add it to the list
return false;
}
}
}
@@ -0,0 +1,61 @@
using System;
using System.Linq;
using Serilog.Events;
using Serilog.Filters.Expressions;
namespace Umbraco.Core.Logging.Viewer
{
//Log Expression Filters (pass in filter exp string)
internal class ExpressionFilter : ILogFilter
{
private readonly Func<LogEvent, bool> _filter;
private const string ExpressionOperators = "()+=*<>%-";
public ExpressionFilter(string filterExpression)
{
Func<LogEvent, bool> filter;
if (string.IsNullOrEmpty(filterExpression))
{
return;
}
// If the expression is one word and doesn't contain a serilog operator then we can perform a like search
if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c)))
{
filter = PerformMessageLikeFilter(filterExpression);
}
else // check if it's a valid expression
{
// If the expression evaluates then make it into a filter
if (FilterLanguage.TryCreateFilter(filterExpression, out var eval, out _))
{
filter = evt => true.Equals(eval(evt));
}
else
{
//Assume the expression was a search string and make a Like filter from that
filter = PerformMessageLikeFilter(filterExpression);
}
}
_filter = filter;
}
public bool TakeLogEvent(LogEvent e)
{
return _filter == null || _filter(e);
}
private Func<LogEvent, bool> PerformMessageLikeFilter(string filterExpression)
{
var filterSearch = $"@Message like '%{FilterLanguage.EscapeLikeExpressionContent(filterExpression)}%'";
if (FilterLanguage.TryCreateFilter(filterSearch, out var eval, out _))
{
return evt => true.Equals(eval(evt));
}
return null;
}
}
}
@@ -0,0 +1,9 @@
using Serilog.Events;
namespace Umbraco.Core.Logging.Viewer
{
public interface ILogFilter
{
bool TakeLogEvent(LogEvent e);
}
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Logging.Viewer
{
public interface ILogViewer
{
/// <summary>
/// Get all saved searches from your chosen data source
/// </summary>
IReadOnlyList<SavedLogSearch> GetSavedSearches();
/// <summary>
/// Adds a new saved search to chosen data source and returns the updated searches
/// </summary>
IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query);
/// <summary>
/// Deletes a saved search to chosen data source and returns the remaining searches
/// </summary>
IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query);
/// <summary>
/// A count of number of errors
/// By counting Warnings with Exceptions, Errors & Fatal messages
/// </summary>
int GetNumberOfErrors(DateTimeOffset startDate, DateTimeOffset endDate);
/// <summary>
/// Returns a number of the different log level entries
/// </summary>
LogLevelCounts GetLogLevelCounts(DateTimeOffset startDate, DateTimeOffset endDate);
/// <summary>
/// Returns a list of all unique message templates and their counts
/// </summary>
IEnumerable<LogTemplate> GetMessageTemplates(DateTimeOffset startDate, DateTimeOffset endDate);
bool CanHandleLargeLogs { get; }
bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate);
/// <summary>
/// Returns the collection of logs
/// </summary>
PagedResult<LogMessage> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate,
int pageNumber = 1,
int pageSize = 100,
Direction orderDirection = Direction.Descending,
string filterExpression = null,
string[] logLevels = null);
}
}
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
namespace Umbraco.Core.Logging.Viewer
{
internal class JsonLogViewer : LogViewerSourceBase
{
private readonly string _logsPath;
public JsonLogViewer(string logsPath = "", string searchPath = "") : base(searchPath)
{
if (string.IsNullOrEmpty(logsPath))
logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
_logsPath = logsPath;
}
private const int FileSizeCap = 100;
public override bool CanHandleLargeLogs => false;
public override bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate)
{
//Log Directory
var logDirectory = _logsPath;
//Number of entries
long fileSizeCount = 0;
//foreach full day in the range - see if we can find one or more filenames that end with
//yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
for (var day = startDate.Date; day.Date <= endDate.Date; day = day.AddDays(1))
{
//Filename ending to search for (As could be multiple)
var filesToFind = $"*{day:yyyyMMdd}.json";
var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);
fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length);
}
//The GetLogSize call on JsonLogViewer returns the total file size in bytes
//Check if the log size is not greater than 100Mb (FileSizeCap)
var logSizeAsMegabytes = fileSizeCount / 1024 / 1024;
return logSizeAsMegabytes <= FileSizeCap;
}
protected override IReadOnlyList<LogEvent> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, ILogFilter filter, int skip, int take)
{
var logs = new List<LogEvent>();
//Log Directory
var logDirectory = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
var count = 0;
//foreach full day in the range - see if we can find one or more filenames that end with
//yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
for (var day = startDate.Date; day.Date <= endDate.Date; day = day.AddDays(1))
{
//Filename ending to search for (As could be multiple)
var filesToFind = $"*{day:yyyyMMdd}.json";
var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);
//Foreach file we find - open it
foreach (var filePath in filesForCurrentDay)
{
//Open log file & add contents to the log collection
//Which we then use LINQ to page over
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var stream = new StreamReader(fs))
{
var reader = new LogEventReader(stream);
while (reader.TryRead(out var evt))
{
if (count > skip + take)
{
break;
}
if (count < skip)
{
count++;
continue;
}
if (filter.TakeLogEvent(evt))
{
logs.Add(evt);
}
count++;
}
}
}
}
}
return logs;
}
}
}
@@ -0,0 +1,15 @@
namespace Umbraco.Core.Logging.Viewer
{
public class LogLevelCounts
{
public int Information { get; set; }
public int Debug { get; set; }
public int Warning { get; set; }
public int Error { get; set; }
public int Fatal { get; set; }
}
}
@@ -0,0 +1,43 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Serilog.Events;
using System;
using System.Collections.Generic;
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Umbraco.Core.Logging.Viewer
{
public class LogMessage
{
/// <summary>
/// The time at which the log event occurred.
/// </summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>
/// The level of the event.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public LogEventLevel Level { get; set; }
/// <summary>
/// The message template describing the log event.
/// </summary>
public string MessageTemplateText { get; set; }
/// <summary>
/// The message template filled with the log event properties.
/// </summary>
public string RenderedMessage { get; set; }
/// <summary>
/// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate.
/// </summary>
public IReadOnlyDictionary<string, LogEventPropertyValue> Properties { get; set; }
/// <summary>
/// An exception associated with the log event, or null.
/// </summary>
public string Exception { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace Umbraco.Core.Logging.Viewer
{
public class LogTemplate
{
public string MessageTemplate { get; set; }
public int Count { get; set; }
}
}
@@ -0,0 +1,14 @@
using Umbraco.Core.Components;
namespace Umbraco.Core.Logging.Viewer
{
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
// ReSharper disable once UnusedMember.Global
public class LogViewerComposer : ICoreComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique<ILogViewer>(_ => new JsonLogViewer());
}
}
}
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Serilog.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Logging.Viewer
{
public abstract class LogViewerSourceBase : ILogViewer
{
protected LogViewerSourceBase(string pathToSearches = "")
{
if (string.IsNullOrEmpty(pathToSearches))
// ReSharper disable once StringLiteralTypo
pathToSearches = IOHelper.MapPath("~/Config/logviewer.searches.config.js");
_searchesConfigPath = pathToSearches;
}
private readonly string _searchesConfigPath;
public abstract bool CanHandleLargeLogs { get; }
/// <summary>
/// Get all logs from your chosen data source back as Serilog LogEvents
/// </summary>
protected abstract IReadOnlyList<LogEvent> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, ILogFilter filter, int skip, int take);
public abstract bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate);
public virtual IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]");
var rawJson = System.IO.File.ReadAllText(_searchesConfigPath);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
public virtual IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Add the new item to the bottom of the list
searches.Add(new SavedLogSearch { Name = name, Query = query });
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]");
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfigPath, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
//As opposed to push a new item into the array
return searches;
}
public virtual IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Removes the search
searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query));
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfigPath, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
return searches;
}
public int GetNumberOfErrors(DateTimeOffset startDate, DateTimeOffset endDate)
{
var errorCounter = new ErrorCounterFilter();
GetLogs(startDate, endDate, errorCounter, 0, int.MaxValue);
return errorCounter.Count;
}
public LogLevelCounts GetLogLevelCounts(DateTimeOffset startDate, DateTimeOffset endDate)
{
var counter = new CountingFilter();
GetLogs(startDate, endDate, counter, 0, int.MaxValue);
return counter.Counts;
}
public IEnumerable<LogTemplate> GetMessageTemplates(DateTimeOffset startDate, DateTimeOffset endDate)
{
var messageTemplates = new MessageTemplateFilter();
GetLogs(startDate, endDate, messageTemplates, 0, int.MaxValue);
var templates = messageTemplates.Counts.
Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value })
.OrderByDescending(x=> x.Count);
return templates;
}
public PagedResult<LogMessage> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate,
int pageNumber = 1, int pageSize = 100,
Direction orderDirection = Direction.Descending,
string filterExpression = null,
string[] logLevels = null)
{
var expression = new ExpressionFilter(filterExpression);
var filteredLogs = GetLogs(startDate, endDate, expression, 0, int.MaxValue);
//This is user used the checkbox UI to toggle which log levels they wish to see
//If an empty array or null - its implied all levels to be viewed
if (logLevels?.Length > 0)
{
var logsAfterLevelFilters = new List<LogEvent>();
var validLogType = true;
foreach (var level in logLevels)
{
//Check if level string is part of the LogEventLevel enum
if(Enum.IsDefined(typeof(LogEventLevel), level))
{
validLogType = true;
logsAfterLevelFilters.AddRange(filteredLogs.Where(x => string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase)));
}
else
{
validLogType = false;
}
}
if (validLogType)
{
filteredLogs = logsAfterLevelFilters;
}
}
long totalRecords = filteredLogs.Count;
//Order By, Skip, Take & Select
var 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<LogMessage>(totalRecords, pageNumber, pageSize)
{
Items = logMessages
};
}
private static void EnsureFileExists(string path, string contents)
{
var absolutePath = IOHelper.MapPath(path);
if (System.IO.File.Exists(absolutePath)) return;
using (var writer = System.IO.File.CreateText(absolutePath))
{
writer.Write(contents);
}
}
}
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Serilog.Events;
namespace Umbraco.Core.Logging.Viewer
{
internal class MessageTemplateFilter : ILogFilter
{
public readonly Dictionary<string, int> Counts = new Dictionary<string, int>();
public bool TakeLogEvent(LogEvent e)
{
var templateText = e.MessageTemplate.Text;
if (Counts.TryGetValue(templateText, out var count))
{
count++;
}
else
{
count = 1;
}
Counts[templateText] = count;
//Don't add it to the list
return false;
}
}
}
@@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace Umbraco.Core.Logging.Viewer
{
public class SavedLogSearch
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("query")]
public string Query { get; set; }
}
}
@@ -428,7 +428,7 @@ namespace Umbraco.Core.Migrations.Install
var tableExist = TableExists(tableName);
if (overwrite && tableExist)
{
_logger.Info<DatabaseSchemaCreator>("Table '{TableName}' already exists, but will be recreated", tableName);
_logger.Info<DatabaseSchemaCreator>("Table {TableName} already exists, but will be recreated", tableName);
DropTable(tableName);
tableExist = false;
@@ -437,13 +437,13 @@ namespace Umbraco.Core.Migrations.Install
if (tableExist)
{
// The table exists and was not recreated/overwritten.
_logger.Info<Database>("Table '{TableName}' already exists - no changes were made", tableName);
_logger.Info<Database>("Table {TableName} already exists - no changes were made", tableName);
return;
}
//Execute the Create Table sql
var created = _database.Execute(new Sql(createSql));
_logger.Info<DatabaseSchemaCreator>("Create Table '{TableName}' ({Created}): \n {Sql}", tableName, created, createSql);
_logger.Info<DatabaseSchemaCreator>("Create Table {TableName} ({Created}): \n {Sql}", tableName, created, createSql);
//If any statements exists for the primary key execute them here
if (string.IsNullOrEmpty(createPrimaryKeySql) == false)
@@ -479,11 +479,12 @@ namespace Umbraco.Core.Migrations.Install
if (overwrite)
{
_logger.Info<Database>("Table '{TableName}' was recreated", tableName);
_logger.Info<Database>("Table {TableName} was recreated", tableName);
}
else
{
_logger.Info<Database>("New table '{TableName}' was created", tableName);
_logger.Info<Database>("New table {TableName} was created", tableName);
}
}
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Models.PublishedContent
}
if (!publishedDataTypes.TryGetValue(id, out var dataType))
throw new ArgumentException("Not a valid datatype identifier.", nameof(id));
throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id));
return dataType;
}
+16
View File
@@ -87,6 +87,9 @@
<PackageReference Include="Serilog.Formatting.Compact">
<Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Formatting.Compact.Reader">
<Version>1.0.2</Version>
</PackageReference>
<PackageReference Include="Serilog.Settings.AppSettings">
<Version>2.2.2</Version>
</PackageReference>
@@ -343,6 +346,17 @@
<Compile Include="IO\MediaPathSchemes\TwoGuidsMediaPathScheme.cs" />
<Compile Include="IO\SupportingFileSystems.cs" />
<Compile Include="KeyValuePairExtensions.cs" />
<Compile Include="Logging\Viewer\CountingFilter.cs" />
<Compile Include="Logging\Viewer\ErrorCounterFilter.cs" />
<Compile Include="Logging\Viewer\ExpressionFilter.cs" />
<Compile Include="Logging\Viewer\ILogFilter.cs" />
<Compile Include="Logging\Viewer\LogTemplate.cs" />
<Compile Include="Logging\Viewer\ILogViewer.cs" />
<Compile Include="Logging\Viewer\LogLevelCounts.cs" />
<Compile Include="Logging\Viewer\JsonLogViewer.cs" />
<Compile Include="Logging\Viewer\LogMessage.cs" />
<Compile Include="Logging\Viewer\LogViewerComposer.cs" />
<Compile Include="Logging\Viewer\LogViewerSourceBase.cs" />
<Compile Include="Logging\IProfilingLogger.cs" />
<Compile Include="Logging\LogHttpRequest.cs" />
<Compile Include="Logging\LogLevel.cs" />
@@ -352,6 +366,8 @@
<Compile Include="Logging\Serilog\Enrichers\HttpSessionIdEnricher.cs" />
<Compile Include="Logging\Serilog\LoggerConfigExtensions.cs" />
<Compile Include="Logging\Serilog\Enrichers\Log4NetLevelMapperEnricher.cs" />
<Compile Include="Logging\Viewer\MessageTemplateFilter.cs" />
<Compile Include="Logging\Viewer\SavedLogSearch.cs" />
<Compile Include="Manifest\DashboardAccessRuleConverter.cs" />
<Compile Include="Manifest\ManifestBackOfficeSection.cs" />
<Compile Include="Manifest\ManifestContentAppDefinition.cs" />
@@ -90,7 +90,7 @@ namespace Umbraco.Tests.Composing
Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree]
typesFound = TypeFinder.FindClassesWithAttribute<TreeAttribute>(new[] { typeof (UmbracoContext).Assembly });
Assert.AreEqual(21, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree]
Assert.AreEqual(22, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree]
}
private static IProfilingLogger GetTestProfilingLogger()
+214
View File
@@ -0,0 +1,214 @@
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;
using Umbraco.Core.IO;
using Umbraco.Core.Logging.Viewer;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Tests.Logging
{
[TestFixture]
public class LogviewerTests
{
private ILogViewer _logViewer;
const string _logfileName = "UmbracoTraceLog.UNITTEST.20181112.json";
const string _searchfileName = "logviewer.searches.config.js";
private string _newLogfilePath;
private string _newLogfileDirPath;
private string _newSearchfilePath;
private string _newSearchfileDirPath;
private DateTimeOffset _startDate = new DateTime(year: 2018, month: 11, day: 12, hour:0, minute:0, second:0);
private DateTimeOffset _endDate = new DateTime(year: 2018, month: 11, day: 13, hour: 0, minute: 0, second: 0);
[OneTimeSetUp]
public void Setup()
{
//Create an example JSON log file to check results
//As a one time setup for all tets in this class/fixture
var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName);
_newLogfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"App_Data\Logs\");
_newLogfilePath = Path.Combine(_newLogfileDirPath, _logfileName);
var exampleSearchfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _searchfileName);
_newSearchfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Config\");
_newSearchfilePath = Path.Combine(_newSearchfileDirPath, _searchfileName);
//Create/ensure Directory exists
IOHelper.EnsurePathExists(_newLogfileDirPath);
IOHelper.EnsurePathExists(_newSearchfileDirPath);
//Copy the sample files
File.Copy(exampleLogfilePath, _newLogfilePath, true);
File.Copy(exampleSearchfilePath, _newSearchfilePath, true);
_logViewer = new JsonLogViewer(logsPath: _newLogfileDirPath, searchPath: _newSearchfilePath);
}
[OneTimeTearDown]
public void TearDown()
{
//Cleanup & delete the example log & search files off disk
//Once all tests in this class/fixture have run
if (File.Exists(_newLogfilePath))
File.Delete(_newLogfilePath);
if (File.Exists(_newSearchfilePath))
File.Delete(_newSearchfilePath);
}
[Test]
public void Logs_Contain_Correct_Error_Count()
{
var numberOfErrors = _logViewer.GetNumberOfErrors(startDate: _startDate, endDate: _endDate);
//Our dummy log should contain 2 errors
Assert.AreEqual(2, numberOfErrors);
}
[Test]
public void Logs_Contain_Correct_Log_Level_Counts()
{
var logCounts = _logViewer.GetLogLevelCounts(startDate: _startDate, endDate: _endDate);
Assert.AreEqual(1954, logCounts.Debug);
Assert.AreEqual(2, logCounts.Error);
Assert.AreEqual(0, logCounts.Fatal);
Assert.AreEqual(62, logCounts.Information);
Assert.AreEqual(7, logCounts.Warning);
}
[Test]
public void Logs_Contains_Correct_Message_Templates()
{
var templates = _logViewer.GetMessageTemplates(startDate: _startDate, endDate: _endDate);
//Count no of templates
Assert.AreEqual(43, templates.Count());
//Verify all templates & counts are unique
CollectionAssert.AllItemsAreUnique(templates);
//Ensure the collection contains LogTemplate objects
CollectionAssert.AllItemsAreInstancesOfType(templates, typeof(LogTemplate));
//Get first item & verify its template & count are what we expect
var popularTemplate = templates.FirstOrDefault();
Assert.IsNotNull(popularTemplate);
Assert.AreEqual("{LogPrefix} Task added {TaskType}", popularTemplate.MessageTemplate);
Assert.AreEqual(689, popularTemplate.Count);
}
[Test]
public void Logs_Can_Open_As_Small_File()
{
//We are just testing a return value (as we know the example file is less than 200MB)
//But this test method does not test/check that
var canOpenLogs = _logViewer.CheckCanOpenLogs(startDate: _startDate, endDate: _endDate);
Assert.IsTrue(canOpenLogs);
}
[Test]
public void Logs_Can_Be_Queried()
{
//Should get me the most 100 recent log entries & using default overloads for remaining params
var allLogs = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1);
//Check we get 100 results back for a page & total items all good :)
Assert.AreEqual(100, allLogs.Items.Count());
Assert.AreEqual(2410, allLogs.TotalItems);
Assert.AreEqual(25, allLogs.TotalPages);
//Check collection all contain same object type
CollectionAssert.AllItemsAreInstancesOfType(allLogs.Items, typeof(LogMessage));
//Check first item is newest
var newestItem = allLogs.Items.First();
DateTimeOffset newDate;
DateTimeOffset.TryParse("2018-11-12T09:24:27.4057583Z", out newDate);
Assert.AreEqual(newDate, newestItem.Timestamp);
//Check we call method again with a smaller set of results & in ascending
var smallQuery = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, pageSize: 10, orderDirection: Direction.Ascending);
Assert.AreEqual(10, smallQuery.Items.Count());
Assert.AreEqual(241, smallQuery.TotalPages);
//Check first item is oldest
var oldestItem = smallQuery.Items.First();
DateTimeOffset oldDate;
DateTimeOffset.TryParse("2018-11-12T08:34:45.8371142Z", out oldDate);
Assert.AreEqual(oldDate, oldestItem.Timestamp);
//Check invalid log levels
//Rather than expect 0 items - get all items back & ignore the invalid levels
string[] invalidLogLevels = { "Invalid", "NotALevel" };
var queryWithInvalidLevels = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, logLevels: invalidLogLevels);
Assert.AreEqual(2410, queryWithInvalidLevels.TotalItems);
//Check we can call method with an array of logLevel (error & warning)
string [] logLevels = { "Warning", "Error" };
var queryWithLevels = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, logLevels: logLevels);
Assert.AreEqual(9, queryWithLevels.TotalItems);
//Query @Level='Warning' BUT we pass in array of LogLevels for Debug & Info (Expect to get 0 results)
string[] logLevelMismatch = { "Debug", "Information" };
var filterLevelQuery = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); ;
Assert.AreEqual(0, filterLevelQuery.TotalItems);
}
[TestCase("", 2410)]
[TestCase("Has(@Exception)", 2)]
[TestCase("Has(Duration) and Duration > 1000", 13)]
[TestCase("Not(@Level = 'Verbose') and Not(@Level= 'Debug')", 71)]
[TestCase("StartsWith(SourceContext, 'Umbraco.Core')", 1183)]
[TestCase("@MessageTemplate = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 622)]
[TestCase("SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", 1)]
[TestCase("Contains(SortedComponentTypes[?], 'DatabaseServer')", 1)]
[Test]
public void Logs_Can_Query_With_Expressions(string queryToVerify, int expectedCount)
{
var testQuery = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, filterExpression: queryToVerify);
Assert.AreEqual(expectedCount, testQuery.TotalItems);
}
[Test]
public void Log_Search_Can_Persist()
{
//Add a new search
_logViewer.AddSavedSearch("Unit Test Example", "Has(UnitTest)");
var searches = _logViewer.GetSavedSearches();
var savedSearch = new SavedLogSearch
{
Name = "Unit Test Example",
Query = "Has(UnitTest)"
};
//Check if we can find the newly added item from the results we get back
var findItem = searches.Where(x => x.Name == "Unit Test Example" && x.Query == "Has(UnitTest)");
Assert.IsNotNull(findItem, "We should have found the saved search, but get no results");
Assert.AreEqual(1, findItem.Count(), "Our list of searches should only contain one result");
//TODO: Need someone to help me find out why these don't work
//CollectionAssert.Contains(searches, savedSearch, "Can not find the new search that was saved");
//Assert.That(searches, Contains.Item(savedSearch));
//Remove the search from above & ensure it no longer exists
_logViewer.DeleteSavedSearch("Unit Test Example", "Has(UnitTest)");
searches = _logViewer.GetSavedSearches();
findItem = searches.Where(x => x.Name == "Unit Test Example" && x.Query == "Has(UnitTest)");
Assert.IsEmpty(findItem, "The search item should no longer exist");
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
[
{
"name": "Find all logs where the Level is NOT Verbose and NOT Debug",
"query": "Not(@Level='Verbose') and Not(@Level='Debug')"
},
{
"name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)",
"query": "Has(@Exception)"
},
{
"name": "Find all logs that have the property 'Duration'",
"query": "Has(Duration)"
},
{
"name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
"query": "Has(Duration) and Duration > 1000"
},
{
"name": "Find all logs that are from the namespace 'Umbraco.Core'",
"query": "StartsWith(SourceContext, 'Umbraco.Core')"
},
{
"name": "Find all logs that use a specific log message template",
"query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'"
},
{
"name": "Find logs where one of the items in the SortedComponentTypes property array is equal to",
"query": "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'"
},
{
"name": "Find logs where one of the items in the SortedComponentTypes property array contains",
"query": "Contains(SortedComponentTypes[?], 'DatabaseServer')"
},
{
"name": "Find all logs that the message has localhost in it with SQL like",
"query": "@Message like '%localhost%'"
},
{
"name": "Find all logs that the message that starts with 'end' in it with SQL like",
"query": "@Message like 'end%'"
}
]
+8
View File
@@ -125,6 +125,7 @@
<Compile Include="CoreThings\GuidUtilsTests.cs" />
<Compile Include="CoreThings\HexEncoderTests.cs" />
<Compile Include="CoreXml\RenamedRootNavigatorTests.cs" />
<Compile Include="Logging\LogviewerTests.cs" />
<Compile Include="Manifest\ManifestContentAppTests.cs" />
<Compile Include="Migrations\MigrationPlanTests.cs" />
<Compile Include="Migrations\MigrationTests.cs" />
@@ -514,6 +515,9 @@
<SubType>Designer</SubType>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Logging\UmbracoTraceLog.UNITTEST.20181112.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Packaging\Packages\Document_Type_Picker_1.1.umb" />
<None Include="UmbracoExamine\TestFiles\umbraco-sort.config">
<SubType>Designer</SubType>
@@ -559,6 +563,10 @@
<SubType>Designer</SubType>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Content Include="Logging\logviewer.searches.config.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Migrations\SqlScripts\MySqlTotal-480.sql" />
<Content Include="Migrations\SqlScripts\SqlCe-SchemaAndData-4110.sql" />
<Content Include="Migrations\SqlScripts\SqlCeTotal-480.sql" />
<Content Include="Migrations\SqlScripts\SqlServerTotal-480.sql" />
View File
@@ -0,0 +1,108 @@
/**
* @ngdoc service
* @name umbraco.resources.logViewerResource
* @description Retrives Umbraco log items (by default from JSON files on disk)
*
*
**/
function logViewerResource($q, $http, umbRequestHelper) {
//the factory object returned
return {
getNumberOfErrors: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetNumberOfErrors")),
'Failed to retrieve number of errors in logs');
},
getLogLevelCounts: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetLogLevelCounts")),
'Failed to retrieve log level counts');
},
getMessageTemplates: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetMessageTemplates")),
'Failed to retrieve log templates');
},
getSavedSearches: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetSavedSearches")),
'Failed to retrieve saved searches');
},
postSavedSearch: function (name, query) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"PostSavedSearch"), { 'name': name, 'query': query }),
'Failed to add new saved search');
},
deleteSavedSearch: function (name, query) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"DeleteSavedSearch"), { 'name': name, 'query': query }),
'Failed to delete saved search');
},
getLogs: function (options) {
var defaults = {
pageSize: 100,
pageNumber: 1,
orderDirection: "Descending",
filterExpression: ''
};
if (options === undefined) {
options = {};
}
//overwrite the defaults if there are any specified
angular.extend(defaults, options);
//now copy back to the options we will use
options = defaults;
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetLogs",
options)),
'Failed to retrieve common log messages');
},
canViewLogs: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetCanViewLogs")),
'Failed to retrieve state if logs can be viewed');
}
};
}
angular.module('umbraco.resources').factory('logViewerResource', logViewerResource);
@@ -27,9 +27,10 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
});
//A list of query strings defined that when changed will not cause a reload of the route
var nonRoutingQueryStrings = ["mculture", "cculture"];
var retainedQueryStrings = ['mculture'];
var nonRoutingQueryStrings = ["mculture", "cculture", "lq"];
var retainedQueryStrings = ["mculture"];
function setMode(mode) {
switch (mode) {
case 'tree':
@@ -0,0 +1,11 @@
<div>
<p>
<strong>Query:</strong><br/>
{{ model.query }}
</p>
<p>
<strong>Name:</strong><br/>
<input ng-model="model.name" type="text" name="queryName" ng-change="model.disableSubmitButton = model.name.length === 0"/>
</p>
</div>
@@ -1,18 +1,5 @@
<div class="umb-editors">
<!-- FIXME -->
<!--
For some reason I can't get the animation to work when we use the umb-editor directive.
The the template isn't rendered quick enough and we can't access the css classes inside the directive
I have also tried with the animation service but I can't get the animations to run when the ng-repeat is on the umb-editor directive
<umb-editor
ng-repeat="editor in editors"
model="editor">
</umb-editor>
-->
<!-- Temp solution -->
<div class="umb-editor" ng-repeat="model in editors" ng-style="model.style" ng-class="{'umb-editor--small': model.size === 'small', 'umb-editor--animating': model.animating}">
<div ng-if="!model.view && !model.animating" ng-transclude></div>
<div ng-if="model.view && !model.animating" ng-include="model.view"></div>
@@ -0,0 +1,122 @@
(function () {
"use strict";
function LogViewerOverviewController($q, $location, logViewerResource) {
var vm = this;
vm.loading = false;
vm.canLoadLogs = false;
vm.searches = [];
vm.numberOfErrors = 0;
vm.commonLogMessages = [];
vm.commonLogMessagesCount = 10;
// ChartJS Options - for count/overview of log distribution
vm.logTypeLabels = ["Info", "Debug", "Warning", "Error", "Critical"];
vm.logTypeData = [0, 0, 0, 0, 0];
vm.logTypeColors = [ '#dcdcdc', '#97bbcd', '#46bfbd', '#fdb45c', '#f7464a'];
vm.chartOptions = {
legend: {
display: true,
position: 'left'
}
};
//functions
vm.searchLogQuery = searchLogQuery;
vm.findMessageTemplate = findMessageTemplate;
function preFlightCheck(){
vm.loading = true;
//Do our pre-flight check (to see if we can view logs)
//IE the log file is NOT too big such as 1GB & crash the site
logViewerResource.canViewLogs().then(function(result){
vm.loading = false;
vm.canLoadLogs = result;
if(result){
//Can view logs - so initalise
init();
}
});
}
function init() {
vm.loading = true;
var savedSearches = logViewerResource.getSavedSearches().then(function (data) {
vm.searches = data;
},
// fallback to some defaults if error from API response
function () {
vm.searches = [
{
"name": "Find all logs where the Level is NOT Verbose and NOT Debug",
"query": "Not(@Level='Verbose') and Not(@Level='Debug')"
},
{
"name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)",
"query": "Has(@Exception)"
},
{
"name": "Find all logs that have the property 'Duration'",
"query": "Has(Duration)"
},
{
"name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
"query": "Has(Duration) and Duration > 1000"
},
{
"name": "Find all logs that are from the namespace 'Umbraco.Core'",
"query": "StartsWith(SourceContext, 'Umbraco.Core')"
},
{
"name": "Find all logs that use a specific log message template",
"query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'"
}
]
});
var numOfErrors = logViewerResource.getNumberOfErrors().then(function (data) {
vm.numberOfErrors = data;
});
var logCounts = logViewerResource.getLogLevelCounts().then(function (data) {
vm.logTypeData = [];
vm.logTypeData.push(data.Information);
vm.logTypeData.push(data.Debug);
vm.logTypeData.push(data.Warning);
vm.logTypeData.push(data.Error);
vm.logTypeData.push(data.Fatal);
});
var commonMsgs = logViewerResource.getMessageTemplates().then(function(data){
vm.commonLogMessages = data;
});
//Set loading indicatior to false when these 3 queries complete
$q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function(data) {
vm.loading = false;
});
}
function searchLogQuery(logQuery){
$location.path("/settings/logViewer/search").search({lq: logQuery});
}
function findMessageTemplate(template){
var logQuery = "@MessageTemplate='" + template.MessageTemplate + "'";
searchLogQuery(logQuery);
}
preFlightCheck();
}
angular.module("umbraco").controller("Umbraco.Editors.LogViewer.OverviewController", LogViewerOverviewController);
})();
@@ -0,0 +1,96 @@
<div data-element="editor-logs" ng-controller="Umbraco.Editors.LogViewer.OverviewController as vm" class="clearfix">
<umb-editor-view footer="false">
<umb-editor-header
name="'Log Overview for Today'"
name-locked="true"
hide-icon="true"
hide-description="true"
hide-alias="true">
</umb-editor-header>
<umb-editor-container>
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<!-- Warning message (if unable to view log files) -->
<div ng-show="!vm.loading && !vm.canLoadLogs">
<umb-box>
<umb-box-header title="Unable to view logs"/>
<umb-box-content>
<p>Today's log file is too large to be viewed and would cause performance problems.</p>
<p>If you need to view the log files, try opening them manually</p>
</umb-box-content>
</umb-box>
</div>
<div class="umb-package-details" ng-show="!vm.loading && vm.canLoadLogs">
<div class="umb-package-details__main-content">
<!-- Saved Searches -->
<umb-box>
<umb-box-header>Saved Searches</umb-box-header>
<umb-box-content>
<table>
<tr>
<td>
<a ng-click="vm.searchLogQuery()" title="View all Logs" class="btn btn-link">All Logs <i class="icon-search"></i></a>
</td>
</tr>
<!-- Fetch saved searches -->
<tr ng-repeat="search in vm.searches">
<td>
<a ng-click="vm.searchLogQuery(search.query)" title="{{search.name}}" class="btn btn-link">{{search.name}} <i class="icon-search"></i></a>
</td>
</tr>
</table>
</umb-box-content>
</umb-box>
<!-- List of top X common log messages -->
<umb-box>
<umb-box-header>Common Log Messages</umb-box-header>
<umb-box-content class="block-form">
<em>Total Unique Message types</em>: {{ vm.commonLogMessages.length }}
<table class="table table-hover">
<tbody>
<tr ng-repeat="template in vm.commonLogMessages | limitTo:vm.commonLogMessagesCount" ng-click="vm.findMessageTemplate(template)" style="cursor: pointer;">
<td>
{{ template.MessageTemplate }}
</td>
<td>
{{ template.Count }}
</td>
</tr>
</tbody>
</table>
<a class="btn btn-primary" ng-if="vm.commonLogMessagesCount < vm.commonLogMessages.length" ng-click="vm.commonLogMessagesCount = vm.commonLogMessagesCount +10">Show More</a>
</umb-box-content>
</umb-box>
</div>
<div class="umb-package-details__sidebar">
<!-- No of Errors -->
<umb-box ng-click="vm.searchLogQuery('Has(@Exception)')" style="cursor:pointer;">
<umb-box-header>Number of Errors</umb-box-header>
<umb-box-content class="block-form" style="font-size: 40px; font-weight:900; text-align:center; color:#fe6561;">
{{ vm.numberOfErrors }}
</umb-box-content>
</umb-box>
<!-- Chart of diff log types -->
<umb-box>
<umb-box-header>Log Types</umb-box-header>
<umb-box-content class="block-form">
<canvas chart-doughnut
chart-data="vm.logTypeData"
chart-labels="vm.logTypeLabels"
chart-colors="vm.logTypeColors"
chart-options="vm.chartOptions"></canvas>
</umb-box-content>
</umb-box>
</div>
</div>
</umb-editor-container>
</umb-editor-view>
</div>
@@ -0,0 +1,324 @@
(function () {
"use strict";
function LogViewerSearchController($location, logViewerResource, overlayService) {
var vm = this;
vm.loading = false;
vm.logsLoading = false;
vm.showBackButton = true;
vm.page = {};
vm.logLevels = [
{
name: 'Verbose',
logTypeColor: 'gray'
},
{
name: 'Debug',
logTypeColor: 'secondary'
},
{
name: 'Information',
logTypeColor: 'primary'
},
{
name: 'Warning',
logTypeColor: 'warning'
},
{
name: 'Error',
logTypeColor: 'danger'
},
{
name: 'Fatal',
logTypeColor: 'danger'
}
];
vm.searches = [];
vm.logItems = {};
vm.logOptions = {};
vm.logOptions.orderDirection = 'Descending';
vm.fromDatePickerConfig = {
pickDate: true,
pickTime: true,
useSeconds: false,
useCurrent: false,
format: "YYYY-MM-DD HH:mm",
icons: {
time: "icon-time",
date: "icon-calendar",
up: "icon-chevron-up",
down: "icon-chevron-down"
}
};
vm.toDatePickerConfig = {
pickDate: true,
pickTime: true,
useSeconds: false,
format: "YYYY-MM-DD HH:mm",
icons: {
time: "icon-time",
date: "icon-calendar",
up: "icon-chevron-up",
down: "icon-chevron-down"
}
};
//Functions
vm.getLogs = getLogs;
vm.changePageNumber = changePageNumber;
vm.search = search;
vm.getFilterName = getFilterName;
vm.setLogLevelFilter = setLogLevelFilter;
vm.toggleOrderBy = toggleOrderBy;
vm.selectSearch = selectSearch;
vm.resetSearch = resetSearch;
vm.findItem = findItem;
vm.checkForSavedSearch = checkForSavedSearch;
vm.addToSavedSearches = addToSavedSearches;
vm.deleteSavedSearch = deleteSavedSearch;
vm.back = back;
function init() {
//If we have a Querystring set for lq (log query)
//Then update vm.logOptions.filterExpression
var querystring = $location.search();
if(querystring.lq){
vm.logOptions.filterExpression = querystring.lq;
}
vm.loading = true;
logViewerResource.getSavedSearches().then(function (data) {
vm.searches = data;
vm.loading = false;
},
// fallback to some defaults if error from API response
function () {
vm.searches = [
{
"name": "Find all logs where the Level is NOT Verbose and NOT Debug",
"query": "Not(@Level='Verbose') and Not(@Level='Debug')"
},
{
"name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)",
"query": "Has(@Exception)"
},
{
"name": "Find all logs that have the property 'Duration'",
"query": "Has(Duration)"
},
{
"name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
"query": "Has(Duration) and Duration > 1000"
},
{
"name": "Find all logs that are from the namespace 'Umbraco.Core'",
"query": "StartsWith(SourceContext, 'Umbraco.Core')"
},
{
"name": "Find all logs that use a specific log message template",
"query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'"
}
]
});
//Get all logs on init load
getLogs();
}
function search(){
//Update the querystring lq (log query)
$location.search('lq', vm.logOptions.filterExpression);
//Reset pagenumber back to 1
vm.logOptions.pageNumber = 1;
getLogs();
}
function changePageNumber(pageNumber) {
vm.logOptions.pageNumber = pageNumber;
getLogs();
}
function getLogs(){
vm.logsLoading = true;
logViewerResource.getLogs(vm.logOptions).then(function (data) {
vm.logItems = data;
vm.logsLoading = false;
setLogTypeColor(vm.logItems.items);
}, function(err){
vm.logsLoading = false;
});
}
function setLogTypeColor(logItems) {
angular.forEach(logItems, function (log) {
switch (log.Level) {
case "Information":
log.logTypeColor = "primary";
break;
case "Debug":
log.logTypeColor = "secondary";
break;
case "Warning":
log.logTypeColor = "warning";
break;
case "Fatal":
case "Error":
log.logTypeColor = "danger";
break;
default:
log.logTypeColor = "gray";
}
});
}
function getFilterName(array) {
var name = "All";
var found = false;
angular.forEach(array, function (item) {
if (item.selected) {
if (!found) {
name = item.name
found = true;
} else {
name = name + ", " + item.name;
}
}
});
return name;
}
function setLogLevelFilter(logLevel) {
if (!vm.logOptions.logLevels) {
vm.logOptions.logLevels = [];
}
if (logLevel.selected) {
vm.logOptions.logLevels.push(logLevel.name);
} else {
var index = vm.logOptions.logLevels.indexOf(logLevel.name);
vm.logOptions.logLevels.splice(index, 1);
}
getLogs();
}
function toggleOrderBy(){
vm.logOptions.orderDirection = vm.logOptions.orderDirection === 'Descending' ? 'Ascending' : 'Descending';
getLogs();
}
function selectSearch(searchItem){
//Update search box input
vm.logOptions.filterExpression = searchItem.query;
vm.dropdownOpen = false;
search();
}
function resetSearch(){
vm.logOptions.filterExpression = '';
search();
}
function findItem(key, value){
if(isNaN(value)){
vm.logOptions.filterExpression = key + "='" + value + "'";
}
else {
vm.logOptions.filterExpression = key + "=" + value;
}
search();
}
//Return a bool to toggle display of the star/fav
function checkForSavedSearch(){
//Check if we have a value in
if(!vm.logOptions.filterExpression){
return false;
}
else {
//Check what we have searched for is not an existing saved search
var findQuery = _.findWhere(vm.searches, {query: vm.logOptions.filterExpression});
return !findQuery ? true: false;
}
}
function addToSavedSearches(){
var overlay = {
title: "Save Search",
subtitle: "Enter a friendly name for your search query",
closeButtonLabel: "Cancel",
submitButtonLabel: "Save Search",
disableSubmitButton: true,
view: "logviewersearch",
query: vm.logOptions.filterExpression,
submit: function (model) {
//Resource call with two params (name & query)
//API that opens the JSON and adds it to the bottom
logViewerResource.postSavedSearch(model.name, model.query).then(function(data){
vm.searches = data;
overlayService.close();
});
},
close: function() {
overlayService.close();
}
};
overlayService.open(overlay);
}
function deleteSavedSearch(searchItem) {
var overlay = {
title: "Delete Search",
subtitle: "Are you sure you wish to delete",
closeButtonLabel: "Cancel",
submitButtonLabel: "Delete Search",
view: "default",
submit: function (model) {
//Resource call with two params (name & query)
//API that opens the JSON and adds it to the bottom
logViewerResource.deleteSavedSearch(searchItem.name, searchItem.query).then(function(data){
vm.searches = data;
overlayService.close();
});
},
close: function() {
overlayService.close();
}
};
overlayService.open(overlay);
}
function back() {
$location.path("settings/logViewer/overview").search('lq', null);
}
init();
}
angular.module("umbraco").controller("Umbraco.Editors.LogViewer.SearchController", LogViewerSearchController);
})();
@@ -0,0 +1,200 @@
<div data-element="editor-logs" ng-controller="Umbraco.Editors.LogViewer.SearchController as vm" class="clearfix">
<umb-editor-view footer="false">
<umb-editor-header
name="'Log Search'"
name-locked="true"
on-back="vm.back()"
show-back-button="vm.showBackButton"
hide-icon="true"
hide-description="true"
hide-alias="true">
</umb-editor-header>
<umb-editor-container>
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<form ng-submit="vm.search()">
<umb-editor-sub-header>
<umb-editor-sub-header-content-left style="width:100%; line-height:30px;">
<!-- Log Level filter -->
<div style="position: relative;">
<a class="btn btn-link dropdown-toggle flex" href="" ng-click="vm.page.showLevelFilter = !vm.page.showLevelFilter">
<span>Log Levels:</span>
<span class="bold truncate dib" style="margin-left: 5px; margin-right: 3px; max-width: 150px;">{{ vm.getFilterName(vm.logLevels) }}</span>
<span class="caret"></span>
</a>
<umb-dropdown class="pull-right" ng-if="vm.page.showLevelFilter" on-close="vm.page.showLevelFilter = false;">
<umb-dropdown-item ng-repeat="level in vm.logLevels" style="padding: 8px 20px 8px 16px;">
<div class="flex items-center">
<input
id="loglevel-{{$index}}"
type="checkbox"
ng-model="level.selected"
ng-change="vm.setLogLevelFilter(level)"
style="margin-right: 10px; margin-top: -3px;" />
<label for="loglevel-{{$index}}">
<umb-badge size="s" color="{{ level.logTypeColor }}">{{ level.name }}</umb-badge>
</label>
</div>
</umb-dropdown-item>
</umb-dropdown>
</div>
</umb-editor-sub-header-content-left>
</umb-editor-sub-header>
<umb-editor-sub-header>
<umb-editor-sub-header-content-left style="width:100%">
<div style="position:relative; width:100%;">
<!-- Search/expression filter -->
<input class="form-control search-input" type="text" ng-model="vm.logOptions.filterExpression" style="width:100%;" placeholder="Search logs&hellip;" />
<!-- Save Search & Clear Search icon buttons -->
<ins class="icon-rate" ng-if="vm.checkForSavedSearch()" ng-click="vm.addToSavedSearches()" style="float: left; position: absolute; top: 0; line-height: 32px; right: 160px; color: #fdb45c; cursor: pointer;">&nbsp;</ins>
<ins class="icon-wrong" ng-if="vm.logOptions.filterExpression" ng-click="vm.resetSearch()" style="float: left; position: absolute; top: 0; line-height: 32px; right: 140px; color: #bbbabf; cursor: pointer;">&nbsp;</ins>
<a class="umb-variant-switcher__toggle ng-scope" href="" ng-click="vm.dropdownOpen = !vm.dropdownOpen" style="top:0;">
<span class="ng-binding">Example Searches</span>
<ins class="umb-variant-switcher__expand icon-navigation-down" ng-class="{'icon-navigation-down': !vm.dropdownOpen, 'icon-navigation-up': vm.dropdownOpen}">&nbsp;</ins>
</a>
<!-- Common Searches Dropdown -->
<umb-dropdown ng-if="vm.dropdownOpen" style="width: 100%; max-height: 250px; overflow-y: scroll; margin-top: -10px;" on-close="vm.dropdownOpen = false" umb-keyboard-list>
<umb-dropdown-item class="umb-variant-switcher__item" ng-class="{'umb-variant-switcher_item--current': variant.active}" ng-repeat="search in vm.searches">
<a href="" class="umb-variant-switcher__name-wrapper" ng-click="vm.selectSearch(search)" prevent-default>
<span class="umb-variant-switcher__name">{{search.name}}</span>
<span>{{ search.query }}</span>
</a>
<a href=""><span><i class="icon icon-trash text-error" title="Delete this search" ng-click="vm.deleteSavedSearch(search)"></i></span></a>
</umb-dropdown-item>
</umb-dropdown>
</div>
<!-- Search Button -->
<umb-button
button-style="button"
type="submit"
action="vm.search()"
label-key="general_search">
</umb-button>
</umb-editor-sub-header-content-left>
</umb-editor-sub-header>
</form>
<div class=" ng-if="!vm.loading">
<div class="">
<!-- Loader for the main logs content when paging -->
<umb-load-indicator ng-if="vm.logsLoading"></umb-load-indicator>
<!-- Empty states -->
<umb-empty-state ng-if="vm.logItems.totalItems === 0" position="center">
<localize key="general_searchNoResult"></localize>
</umb-empty-state>
<!-- Main Log Table -->
<umb-box data-element="node-info-history" ng-if="!vm.logsLoading && vm.logItems.totalItems > 0">
<umb-box-content class="block-form">
<div ng-if="vm.logItems.totalItems > 0">
Total Items: {{ vm.logItems.totalItems }}
</div>
<table class="table table-hover" ng-if="vm.logItems.totalItems > 0">
<thead>
<tr>
<th style="min-width:100px;" ng-click="vm.toggleOrderBy()">
Timestamp
<ins class="icon-navigation-down" ng-class="{'icon-navigation-down': vm.logOptions.orderDirection === 'Descending', 'icon-navigation-up': vm.logOptions.orderDirection !== 'Descending'}">&nbsp;</ins>
</th>
<th>Level</th>
<th>Machine</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr ng-repeat-start="log in vm.logItems.items" ng-click="log.open = !log.open">
<td>{{ log.Timestamp | date:'medium' }}</td>
<td><umb-badge size="s" color="{{ log.logTypeColor }}">{{ log.Level }}</umb-badge></td>
<td><small>{{ log.Properties.MachineName.Value }}</small></td>
<td>{{ log.RenderedMessage }}</td>
</tr>
<!-- Log Details (Exception & Properties) -->
<tr ng-repeat-end ng-if="log.open">
<td colspan="4">
<div ng-if="log.Exception" style="border-left:4px solid #fe6561; padding:0 10px 10px 10px; box-shadow:rgba(0,0,0,0.07) 2px 2px 10px">
<h3 style="color:#fe6561;">Exception</h3>
<p style="white-space: pre-wrap;">{{ log.Exception }}</p>
</div>
<h3>Properties</h3>
<table class="table">
<tbody>
<tr>
<th>Timestamp</th>
<td>{{log.Timestamp}}</td>
</tr>
<tr>
<th>@MessageTemplate</th>
<td>{{log.MessageTemplateText}}</td>
</tr>
<tr ng-repeat="(key, val) in log.Properties">
<th>{{key}}</th>
<td ng-switch on="key">
<a ng-switch-when="HttpRequestNumber" ng-click="vm.findItem(key, val.Value)" title="Find Logs with Request ID">{{val.Value}} <i class="icon-search"></i></a>
<a ng-switch-when="SourceContext" ng-click="vm.findItem(key, val.Value)" title="Find Logs with Namespace">{{val.Value}} <i class="icon-search"></i></a>
<a ng-switch-when="MachineName" ng-click="vm.findItem(key, val.Value)" title="Find Logs with Machine Name">{{val.Value}} <i class="icon-search"></i></a>
<a ng-switch-when="RequestUrl" href="{{val.Value}}" target="_blank" rel="noopener" title="Open">{{val.Value}} <i class="icon-link"></i></a>
<span ng-switch-default>{{val.Value}}</span>
</td>
</tr>
</tbody>
</table>
<div class="btn-group">
<a class="btn btn-info dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-search"></i> Search
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a ng-href="https://www.google.com/search?q={{ log.RenderedMessage }}" target="_blank" title="Search this message with Google"><img src="https://www.google.com/favicon.ico" width="16" height="16" /> Search With Google</a></li>
<li><a ng-href="https://www.bing.com/search?q={{ log.RenderedMessage }}" target="_blank" title="Search this message with Bing"><img src="https://www.bing.com/favicon.ico" width="16" height="16" /> Search With Bing</a></li>
<li><a ng-href="https://our.umbraco.com/search?q={{ log.RenderedMessage }}&content=wiki,forum,documentation" target="_blank" title="Search this message on Our Umbraco forums and docs"><img src="https://our.umbraco.com/assets/images/app-icons/favicon.png" width="16" height="16" /> Search Our Umbraco</a></li>
<li><a ng-href="https://www.google.co.uk/?q=site:our.umbraco.org {{ log.RenderedMessage }}&safe=off#q=site:our.umbraco.org {{ log.RenderedMessage }} {{ log.Properties['SourceContext'].Value }}&safe=off" target="_blank" title="Search Our Umbraco forums using Google"><img src="https://www.google.com/favicon.ico" width="16" height="16" /> Search Our with Google</a></li>
<li><a ng-href="https://github.com/umbraco/Umbraco-CMS/search?q={{ log.Properties['SourceContext'].Value }}" target="_blank" title="Search within Umbraco source code on Github"><img src="https://www.github.com/favicon.ico" width="16" height="16" /> Search Umbraco Source</a></li>
<li><a ng-href="https://github.com/umbraco/Umbraco-CMS/issues?q={{ log.Properties['SourceContext'].Value }}" target="_blank" title="Search Umbraco Issues on Github"><img src="https://www.github.com/favicon.ico" width="16" height="16" /> Search Umbraco Issues</a></li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Pagination -->
<div ng-if="!vm.loading" class="flex justify-center">
<umb-pagination
ng-if="vm.logItems.totalPages"
page-number="vm.logItems.pageNumber"
total-pages="vm.logItems.totalPages"
on-change="vm.changePageNumber(pageNumber)">
</umb-pagination>
</div>
</umb-box-content>
</umb-box>
</div>
</div>
</umb-editor-container>
</umb-editor-view>
</div>
+1
View File
@@ -236,6 +236,7 @@
<Content Include="Config\serilog.config">
<SubType>Designer</SubType>
</Content>
<Content Include="Config\logviewer.searches.config.js" />
<None Include="Config\umbracoSettings.Release.config">
<DependentUpon>umbracoSettings.config</DependentUpon>
<SubType>Designer</SubType>
@@ -1680,6 +1680,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="scripts">Scripts</key>
<key alias="stylesheets">Stylesheets</key>
<key alias="templates">Templates</key>
<key alias="logViewer">Log Viewer</key>
<key alias="users">Users</key>
<key alias="settingsGroup">Settings</key>
<key alias="templatingGroup">Templating</key>
@@ -115,8 +115,6 @@ namespace Umbraco.Web.UI.Umbraco.Developer.Macros
protected IEnumerable<IDataEditor> GetMacroParameterEditors()
{
// we need to show the depracated ones for backwards compatibility
// FIXME not managing deprecated here?!
return Current.ParameterEditors;
}
@@ -0,0 +1,42 @@
[
{
"name": "Find all logs where the Level is NOT Verbose and NOT Debug",
"query": "Not(@Level='Verbose') and Not(@Level='Debug')"
},
{
"name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)",
"query": "Has(@Exception)"
},
{
"name": "Find all logs that have the property 'Duration'",
"query": "Has(Duration)"
},
{
"name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
"query": "Has(Duration) and Duration > 1000"
},
{
"name": "Find all logs that are from the namespace 'Umbraco.Core'",
"query": "StartsWith(SourceContext, 'Umbraco.Core')"
},
{
"name": "Find all logs that use a specific log message template",
"query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'"
},
{
"name": "Find logs where one of the items in the SortedComponentTypes property array is equal to",
"query": "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'"
},
{
"name": "Find logs where one of the items in the SortedComponentTypes property array contains",
"query": "Contains(SortedComponentTypes[?], 'DatabaseServer')"
},
{
"name": "Find all logs that the message has localhost in it with SQL like",
"query": "@Message like '%localhost%'"
},
{
"name": "Find all logs that the message that starts with 'end' in it with SQL like",
"query": "@Message like 'end%'"
}
]
@@ -310,8 +310,12 @@ namespace Umbraco.Web.Editors
controller => controller.GetAllLanguages())
},
{
"relationTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<RelationTypeController>(
"relationTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<RelationTypeController>(
controller => controller.GetById(1))
},
{
"logViewerApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<LogViewerController>(
controller => controller.GetNumberOfErrors())
}
}
},
@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Web.Http;
using Umbraco.Core.Logging.Viewer;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Editors
{
/// <summary>
/// Backoffice controller supporting the dashboard for viewing logs with some simple graphs & filtering
/// </summary>
[PluginController("UmbracoApi")]
public class LogViewerController : UmbracoAuthorizedJsonController
{
private ILogViewer _logViewer;
public LogViewerController(ILogViewer logViewer)
{
_logViewer = logViewer;
}
private bool CanViewLogs()
{
//Can the interface deal with Large Files
if (_logViewer.CanHandleLargeLogs)
return true;
//Interface CheckCanOpenLogs
return _logViewer.CheckCanOpenLogs(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now);
}
[HttpGet]
public bool GetCanViewLogs()
{
return CanViewLogs();
}
[HttpGet]
public int GetNumberOfErrors()
{
//We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs() == false)
{
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"));
}
return _logViewer.GetNumberOfErrors(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now);
}
[HttpGet]
public LogLevelCounts GetLogLevelCounts()
{
//We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs() == false)
{
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"));
}
return _logViewer.GetLogLevelCounts(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now);
}
[HttpGet]
public IEnumerable<LogTemplate> GetMessageTemplates()
{
//We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs() == false)
{
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"));
}
return _logViewer.GetMessageTemplates(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now);
}
[HttpGet]
public PagedResult<LogMessage> GetLogs(string orderDirection = "Descending", int pageNumber = 1, string filterExpression = null, [FromUri]string[] logLevels = null)
{
//We will need to stop the request if trying to do this on a 1GB file
if (CanViewLogs() == false)
{
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size"));
}
var direction = orderDirection == "Descending" ? Direction.Descending : Direction.Ascending;
return _logViewer.GetLogs(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now, filterExpression: filterExpression, pageNumber: pageNumber, orderDirection: direction, logLevels: logLevels);
}
[HttpGet]
public IEnumerable<SavedLogSearch> GetSavedSearches()
{
return _logViewer.GetSavedSearches();
}
[HttpPost]
public IEnumerable<SavedLogSearch> PostSavedSearch(SavedLogSearch item)
{
return _logViewer.AddSavedSearch(item.Name, item.Query);
}
[HttpPost]
public IEnumerable<SavedLogSearch> DeleteSavedSearch(SavedLogSearch item)
{
return _logViewer.DeleteSavedSearch(item.Name, item.Query);
}
}
}
@@ -273,8 +273,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
/// <inheritdoc />
public override PublishedItemType ItemType => _contentNode.ContentType.ItemType;
// fixme
// was => _contentData.Published == false;
/// <inheritdoc />
public override bool IsDraft(string culture = null)
{
@@ -201,7 +201,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void InitializeRepositoryEvents()
{
//fixme: The reason these events are in the repository is for legacy, the events should exist at the service
//todo: The reason these events are in the repository is for legacy, the events should exist at the service
// level now since we can fire these events within the transaction... so move the events to service level
// plug repository event handlers
@@ -584,7 +584,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
continue;
}
// fixme - should we do some RV check here? (later)
// todo- should we do some RV check here? (later)
var capture = payload;
using (var scope = _scopeProvider.CreateScope())
@@ -674,7 +674,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
continue;
}
// fixme - should we do some RV checks here? (later)
// todo- should we do some RV checks here? (later)
var capture = payload;
using (var scope = _scopeProvider.CreateScope())
@@ -773,7 +773,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
using (_contentStore.GetWriter(_scopeProvider))
using (_mediaStore.GetWriter(_scopeProvider))
{
// fixme - need to add a datatype lock
// todo - need to add a datatype lock
// this is triggering datatypes reload in the factory, and right after we create some
// content types by loading them ... there's a race condition here, which would require
// some locking on datatypes
@@ -30,7 +30,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
private Task _collectTask;
private volatile int _wlocked;
// fixme - collection trigger (ok for now)
// minGenDelta to be adjusted
// we may want to throttle collects even if delta is reached
// we may want to force collect if delta is not reached but very old
@@ -107,7 +106,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
// gets a scope contextual representing a locked writer to the dictionary
// fixme GetScopedWriter? should the dict have a ref onto the scope provider?
// GetScopedWriter? should the dict have a ref onto the scope provider?
public IDisposable GetWriter(IScopeProvider scopeProvider)
{
return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new SnapDictionaryWriter(this, scoped));
@@ -12,7 +12,7 @@ namespace Umbraco.Web.PublishedCache
//
// at the moment we do NOT support models for sets - that would require
// an entirely new models factory + not even sure it makes sense at all since
// sets are created manually fixme yes it does!
// sets are created manually todo yes it does! - what does this all mean?
//
internal class PublishedElement : IPublishedElement
{
@@ -25,7 +25,6 @@ namespace Umbraco.Web.PublishedCache
_membershipUser = member;
_publishedMemberType = publishedMemberType ?? throw new ArgumentNullException(nameof(publishedMemberType));
// fixme
// RawValueProperty is used for two things here
// - for the 'map properties' thing that we should really get rid of
// - for populating properties that every member should always have, and that we force-create
@@ -49,8 +48,6 @@ namespace Umbraco.Web.PublishedCache
#region Membership provider member properties
// fixme why this?
public string Email => _membershipUser.Email;
public string UserName => _membershipUser.Username;
@@ -5,7 +5,7 @@ using Umbraco.Core.Scoping;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
// fixme should be a ScopeContextualBase
// todo should be a ScopeContextualBase
internal class SafeXmlReaderWriter : IDisposable
{
private readonly bool _scoped;
@@ -369,7 +369,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
}
catch (InvalidOperationException e)
{
// fixme - enable!
// todo - enable!
//content.Instance.RefreshContentFromDatabase();
throw new InvalidOperationException($"{e.Message}. This usually indicates that the content cache is corrupt; the content cache has been rebuilt in an attempt to self-fix the issue.");
}
@@ -69,7 +69,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// internal for unit tests
// no file nor db, no config check
// fixme - er, we DO have a DB?
// todo - er, we DO have a DB?
internal XmlStore(IContentTypeService contentTypeService, IContentService contentService, IScopeProvider scopeProvider, RoutesCache routesCache, PublishedContentTypeCache contentTypeCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor, MainDom mainDom,
bool testing, bool enableRepositoryEvents, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer)
@@ -602,7 +602,7 @@ AND (umbracoNode.id=@id)";
// should we have async versions that would do: ?
// var releaser = await _xmlLock.LockAsync();
//
// fixme - not sure about the "resync current published snapshot" thing here, see 7.6...
// todo - not sure about the "resync current published snapshot" thing here, see 7.6...
// gets a locked safe read access to the main xml
private SafeXmlReaderWriter GetSafeXmlReader()
@@ -641,7 +641,7 @@ AND (umbracoNode.id=@id)";
public void EnsureFilePermission()
{
// FIXME - but do we really have a store, initialized, at that point?
// todo - but do we really have a store, initialized, at that point?
var filename = _xmlFileName + ".temp";
File.WriteAllText(filename, "TEMP");
File.Delete(filename);
@@ -1559,8 +1559,8 @@ ORDER BY umbracoNode.level, umbracoNode.sortOrder";
// need to update the published xml if we're saving the published version,
// or having an impact on that version - we update the published xml even when masked
// fixme - in the repo... either its 'unpublished' and 'publishing', or 'published' and 'published', this has changed!
// fixme - what are we serializing really? which properties?
// todo - in the repo... either its 'unpublished' and 'publishing', or 'published' and 'published', this has changed!
// todo - what are we serializing really? which properties?
// if not publishing, no change to published xml
if (((Content) entity).PublishedState != PublishedState.Publishing)
@@ -1669,7 +1669,7 @@ ORDER BY umbracoNode.level, umbracoNode.sortOrder";
// RepositoryCacheMode.Scoped because we do NOT want to use the L2 cache that may be out-of-sync
// hopefully this does not cause issues and we're not nested in another scope w/different mode
// fixme - well, guess what?
// todo - well, guess what?
// original code made sure the repository used no cache
// now we're using the Scoped scope cache mode
// and then?
@@ -2027,7 +2027,7 @@ AND cmsPreviewXml.nodeId IS NULL OR cmsPreviewXml.xml NOT LIKE '% key=""'
{
// every non-trashed media item should have a corresponding row in cmsContentXml
// and that row should have the key="..." attribute
// fixme - where's the trashed test here?
// todo - where's the trashed test here?
var mediaObjectType = Constants.ObjectTypes.Media;
var db = scope.Database;
@@ -251,7 +251,7 @@ namespace Umbraco.Web
public static IEnumerable<PublishedSearchResult> SearchDescendants(this IPublishedContent content, string term, string indexName = null)
{
//fixme: pass in the IExamineManager
//todo inject examine manager
indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName;
if (!ExamineManager.Instance.TryGetIndex(indexName, out var index))
@@ -272,7 +272,7 @@ namespace Umbraco.Web
public static IEnumerable<PublishedSearchResult> SearchChildren(this IPublishedContent content, string term, string indexName = null)
{
//fixme: pass in the IExamineManager
//todo inject examine manager
indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName;
if (!ExamineManager.Instance.TryGetIndex(indexName, out var index))
@@ -954,7 +954,7 @@ namespace Umbraco.Web
/// </remarks>
public static IEnumerable<IPublishedContent> Children(this IPublishedContent content, string culture = null)
{
if (content == null) throw new ArgumentNullException(nameof(content)); // fixme wtf is this?
if (content == null) throw new ArgumentNullException(nameof(content)); // fixme/task wtf is this?
return content.Children.Where(x =>
{
@@ -1017,7 +1017,7 @@ namespace Umbraco.Web
/// <summary>
/// Gets the first child of the content, of a given content type.
/// </summary>
public static IPublishedContent FirstChild(this IPublishedContent content, string alias, string culture = null) // fixme oops
public static IPublishedContent FirstChild(this IPublishedContent content, string alias, string culture = null) // fixme/task oops
{
return content.Children(culture,alias).FirstOrDefault();
}
@@ -69,25 +69,6 @@ namespace Umbraco.Web
return prop != null && prop.HasValue(culture, segment);
}
// fixme - .Value() refactoring - in progress
// missing variations...
/// <summary>
/// Returns one of two strings depending on whether the content has a value for a property identified by its alias.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="alias">The property alias.</param>
/// <param name="valueIfTrue">The value to return if the content has a value for the property.</param>
/// <param name="valueIfFalse">The value to return if the content has no value for the property.</param>
/// <returns>Either <paramref name="valueIfTrue"/> or <paramref name="valueIfFalse"/> depending on whether the content
/// has a value for the property identified by the alias.</returns>
public static IHtmlString IfValue(this IPublishedElement content, string alias, string valueIfTrue, string valueIfFalse = null)
{
return content.HasValue(alias)
? new HtmlString(valueIfTrue)
: new HtmlString(valueIfFalse ?? string.Empty);
}
#endregion
#region Value
@@ -165,57 +146,6 @@ namespace Umbraco.Web
#endregion
#region Value or Umbraco.Field - WORK IN PROGRESS
// fixme - .Value() refactoring - in progress
// trying to reproduce Umbraco.Field so we can get rid of it
//
// what we want:
// - alt aliases
// - recursion
// - default value
// - before & after (if value)
//
// convertLineBreaks: should be an extension string.ConvertLineBreaks()
// stripParagraphs: should be an extension string.StripParagraphs()
// format: should use the standard .ToString(format)
//
// see UmbracoComponentRenderer.Field - which is ugly ;-(
// recurse first, on each alias (that's how it's done in Field)
//
// there is no strongly typed recurse, etc => needs to be in ModelsBuilder?
// that one can only happen in ModelsBuilder as that's where the attributes are defined
// the attribute that carries the alias is in ModelsBuilder!
//public static TValue Value<TModel, TValue>(this TModel content, Expression<Func<TModel, TValue>> propertySelector, ...)
// where TModel : IPublishedElement
//{
// PropertyInfo pi = GetPropertyFromExpression(propertySelector);
// var attr = pi.GetCustomAttribute<ImplementPropertyAttribute>();
// var alias = attr.Alias;
// return content.Value<TValue>(alias, ...)
//}
// recurse should be implemented via fallback
// todo - that one should be refactored, missing culture and so many things
public static IHtmlString Value<T>(this IPublishedElement content, string aliases, Func<T, string> format, string alt = "")
{
if (format == null) format = x => x.ToString();
var property = aliases.Split(',')
.Where(x => string.IsNullOrWhiteSpace(x) == false)
.Select(x => content.GetProperty(x.Trim()))
.FirstOrDefault(x => x != null);
return property != null
? new HtmlString(format(property.Value<T>()))
: new HtmlString(alt);
}
#endregion
#region ToIndexedArray
public static IndexedArrayItem<TContent>[] ToIndexedArray<TContent>(this IEnumerable<TContent> source)
@@ -96,7 +96,7 @@ namespace Umbraco.Web.Routing
return v.InvariantContains(a1) || v.InvariantContains(a2);
}
// fixme - even with Linq, what happens below has to be horribly slow
// todo - even with Linq, what happens below has to be horribly slow
// but the only solution is to entirely refactor url providers to stop being dynamic
if (rootNodeId > 0)
+2 -2
View File
@@ -107,7 +107,7 @@ namespace Umbraco.Web.Routing
/// <param name="filter">An optional function to filter the list of domains, if more than one applies.</param>
/// <returns>The domain and its normalized uri, that best matches the specified uri and cultures.</returns>
/// <remarks>
/// fixme - must document and explain this all
/// todo - must document and explain this all
/// <para>If <paramref name="uri"/> is null, pick the first domain that matches <paramref name="culture"/>,
/// else the first that matches <paramref name="defaultCulture"/>, else the first one (ordered by id), else null.</para>
/// <para>If <paramref name="uri"/> is not null, look for domains that would be a base uri of the current uri,</para>
@@ -244,7 +244,7 @@ namespace Umbraco.Web.Routing
/// <returns>The domains and their normalized uris, that match the specified uri.</returns>
internal static IEnumerable<DomainAndUri> SelectDomains(IEnumerable<Domain> domains, Uri uri)
{
// fixme where are we matching ?!!?
// todo where are we matching ?!!?
return domains
.Where(d => d.IsWildcard == false)
.Select(d => new DomainAndUri(d, uri))
+3 -4
View File
@@ -19,8 +19,7 @@ using RenderingEngine = Umbraco.Core.RenderingEngine;
namespace Umbraco.Web.Routing
{
// fixme - make this public
// fixme - making sense to have an interface?
// todo - making sense to have an interface?
public class PublishedRouter
{
private readonly IWebRoutingSection _webRoutingSection;
@@ -53,7 +52,7 @@ namespace Umbraco.Web.Routing
GetRolesForLogin = s => Roles.Provider.GetRolesForUser(s);
}
// fixme
// todo
// in 7.7 this is cached in the PublishedContentRequest, which ... makes little sense
// killing it entirely, if we need cache, just implement it properly !!
// this is all soooo weird
@@ -381,7 +380,7 @@ namespace Umbraco.Web.Routing
// NOTE: we could start with what's the current default?
// fixme - bad - we probably should be using the appropriate filesystems!
// todo - bad - we probably should be using the appropriate filesystems!
if (FindTemplateRenderingEngineInDirectory(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.MvcViews)),
alias, new[] { ".cshtml", ".vbhtml" }))
+2 -2
View File
@@ -181,7 +181,7 @@ namespace Umbraco.Web.Routing
/// <inheritdoc />
public virtual IEnumerable<DomainAndUri> MapDomains(IReadOnlyCollection<DomainAndUri> domainAndUris, Uri current, bool excludeDefault, string culture, string defaultCulture)
{
// fixme ignoring cultures entirely?
// todo ignoring cultures entirely?
var currentAuthority = current.GetLeftPart(UriPartial.Authority);
KeyValuePair<string, string[]>[] candidateSites = null;
@@ -279,7 +279,7 @@ namespace Umbraco.Web.Routing
if (domainAndUris == null) throw new ArgumentNullException(nameof(domainAndUris));
if (domainAndUris.Count == 0) throw new ArgumentException("Cannot be empty.", nameof(domainAndUris));
// fixme how shall we deal with cultures?
// todo how shall we deal with cultures?
// we do our best, but can't do the impossible
// get the "default" domain ie the first one for the culture, else the first one (exists, length > 0)
@@ -108,7 +108,7 @@ namespace Umbraco.Web.Runtime
.SetDefaultRenderMvcController<RenderMvcController>(); // default controller for template views
composition.WithCollectionBuilder<SearchableTreeCollectionBuilder>()
.Add(() => composition.TypeLoader.GetTypes<ISearchableTree>()); // fixme which searchable trees?!
.Add(() => composition.TypeLoader.GetTypes<ISearchableTree>());
composition.Register<UmbracoTreeSearcher>(Lifetime.Request);
@@ -15,6 +15,6 @@ namespace Umbraco.Web.Scheduling
void Add(T task);
bool TryAdd(T task);
// fixme - complete the interface?
// todo - complete the interface?
}
}
@@ -56,7 +56,7 @@ namespace Umbraco.Web.Scheduling
// ensure we run with an UmbracoContext, because this may run in a background task,
// yet developers may be using the 'current' UmbracoContext in the event handlers
//
// fixme
// todo
// - or maybe not, CacheRefresherComponent already ensures a context when handling events
// - UmbracoContext 'current' needs to be refactored and cleaned up
// - batched messenger should not depend on a current HttpContext
+1 -2
View File
@@ -154,7 +154,6 @@ namespace Umbraco.Web.Search
_rebuildOnStartupRunner = new BackgroundTaskRunner<IBackgroundTask>(
"RebuildIndexesOnStartup",
//new BackgroundTaskRunnerOptions{ LongRunning= true }, //fixme, this flag doesn't have any affect anymore
logger);
_rebuildOnStartupRunner.TryAdd(task);
@@ -214,7 +213,7 @@ namespace Umbraco.Web.Search
// just ignore that payload
// so what?!
//fixme: Rebuild the index at this point?
//todo: Rebuild the index at this point?
}
else // RefreshNode or RefreshBranch (maybe trashed)
{
+1 -1
View File
@@ -326,7 +326,7 @@ namespace Umbraco.Web.Security
switch (umbracoType)
{
case UmbracoObjectTypes.Member:
// fixme - need to implement Get(guid)!
// todo - need to implement Get(guid)!
var memberAttempt = entityService.GetId(guidUdi.Guid, umbracoType);
if (memberAttempt.Success)
return GetById(memberAttempt.Result);
@@ -37,8 +37,8 @@ namespace Umbraco.Web.Templates
_umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext));
}
private IFileService FileService => Current.Services.FileService; // fixme inject
private PublishedRouter PublishedRouter => Core.Composing.Current.Factory.GetInstance<PublishedRouter>(); // fixme inject
private IFileService FileService => Current.Services.FileService; // todo inject
private PublishedRouter PublishedRouter => Core.Composing.Current.Factory.GetInstance<PublishedRouter>(); // todo inject
/// <summary>
@@ -235,7 +235,6 @@ namespace Umbraco.Web.Trees
AddActionNode<ActionSort>(item, menu, true);
AddActionNode<ActionAssignDomain>(item, menu, opensDialog: true);
AddActionNode<ActionRights>(item, menu, opensDialog: true);
//fixme - conver this editor to angular
AddActionNode<ActionProtect>(item, menu, true, convert: true, opensDialog: true);
if (EmailSender.CanSendRequiredEmail)
{
@@ -308,7 +307,7 @@ namespace Umbraco.Web.Trees
entity.Name = "[[" + entity.Id + "]]";
}
//fixme: Remove the need for converting to legacy
//todo: Remove the need for converting to legacy
private void AddActionNode<TAction>(IUmbracoEntity item, MenuItemCollection menu, bool hasSeparator = false, bool convert = false, bool opensDialog = false)
where TAction : IAction
{
@@ -401,7 +401,7 @@ namespace Umbraco.Web.Trees
internal IEnumerable<MenuItem> GetAllowedUserMenuItemsForNode(IUmbracoEntity dd)
{
var permission = Services.UserService.GetPermissions(Security.CurrentUser, dd.Path);
//fixme: inject
//todo: inject
var actions = Current.Actions.FromEntityPermission(permission)
.ToList();
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Umbraco.Web.Trees
{
//fixme - we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used
//todo- we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used
//leave as internal for now, maybe we'll use in the future, means we could pass around ITree
internal interface ITree
{
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Trees
/// </summary>
internal class LegacyTreeDataConverter
{
//fixme: remove this whole class when everything is angularized
//todo: remove this whole class when everything is angularized
/// <summary>
/// This will look at the legacy IAction's JsFunctionName and convert it to a confirmation dialog view if possible
@@ -0,0 +1,44 @@
using System.Net.Http.Formatting;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.LogViewer)]
[Tree(Constants.Applications.Settings, Constants.Trees.LogViewer, null, sortOrder: 9)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
public class LogViewerTreeController : TreeController
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
//We don't have any child nodes & only use the root node to load a custom UI
return new TreeNodeCollection();
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
//We don't have any menu item options (such as create/delete/reload) & only use the root node to load a custom UI
return null;
}
/// <summary>
/// Helper method to create a root model for a tree
/// </summary>
/// <returns></returns>
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
{
var root = base.CreateRootNode(queryStrings);
//this will load in a custom UI instead of the dashboard for the root node
root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Settings, Constants.Trees.LogViewer, "overview");
root.Icon = "icon-box-alt";
root.HasChildren = false;
root.MenuUrl = null;
return root;
}
}
}
@@ -9,7 +9,7 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Settings, Constants.Trees.Scripts, TreeTitle = "Scripts", IconOpen = "icon-folder", IconClosed = "icon-folder", SortOrder = 10, TreeGroup = Constants.Trees.Groups.Templating)]
public class ScriptsTreeController : FileSystemTreeController
{
protected override IFileSystem FileSystem => Current.FileSystems.ScriptsFileSystem; // fixme inject
protected override IFileSystem FileSystem => Current.FileSystems.ScriptsFileSystem; // todo inject
private static readonly string[] ExtensionsStatic = { "js" };
@@ -8,7 +8,7 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Settings, Constants.Trees.Stylesheets, TreeTitle = "Stylesheets", IconOpen = "icon-folder", IconClosed = "icon-folder", SortOrder = 9, TreeGroup = Constants.Trees.Groups.Templating)]
public class StylesheetsTreeController : FileSystemTreeController
{
protected override IFileSystem FileSystem => Current.FileSystems.StylesheetsFileSystem; // fixme inject
protected override IFileSystem FileSystem => Current.FileSystems.StylesheetsFileSystem; // todo inject
private static readonly string[] ExtensionsStatic = { "css" };
@@ -8,7 +8,6 @@ namespace Umbraco.Web.UI.Controls
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
// fixme - image is gone!
base.ImageUrl = "/images/progressBar.gif";
base.AlternateText = Title;
@@ -20,7 +20,7 @@ namespace Umbraco.Web.UI.Controls
UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext));
Umbraco = new UmbracoHelper(umbracoContext, services);
// fixme inject somehow
// todo inject somehow
Logger = Current.Logger;
ProfilingLogger = Current.ProfilingLogger;
Services = Current.Services;
@@ -31,7 +31,7 @@ namespace Umbraco.Web.UI.Controls
Umbraco = new UmbracoHelper(umbracoContext, services);
Members = Current.Factory.GetInstance<MembershipHelper>();
// fixme inject somehow
// todo inject somehow
Logger = Current.Logger;
ProfilingLogger = Current.ProfilingLogger;
Services = Current.Services;
+2
View File
@@ -219,10 +219,12 @@
<Compile Include="Editors\BackOfficeAssetsController.cs" />
<Compile Include="Editors\BackOfficeModel.cs" />
<Compile Include="Editors\BackOfficeServerVariables.cs" />
<Compile Include="Editors\LogViewerController.cs" />
<Compile Include="ImageProcessorLogger.cs" />
<Compile Include="Macros\MacroTagParser.cs" />
<Compile Include="Models\ContentEditing\ContentDomainsAndCulture.cs" />
<Compile Include="Models\ContentEditing\ContentSavedState.cs" />
<Compile Include="Trees\LogViewerTreeController.cs" />
<Compile Include="Models\Mapping\ContentSavedStateResolver.cs" />
<Compile Include="Mvc\ContainerControllerFactory.cs" />
<Compile Include="OwinExtensions.cs" />
+1 -1
View File
@@ -41,7 +41,7 @@ namespace Umbraco.Web
/// <param name="replace">A value indicating whether to replace the existing context.</param>
/// <returns>The "current" UmbracoContext.</returns>
/// <remarks>
/// fixme - this needs to be clarified
/// todo - this needs to be clarified
///
/// If <paramref name="replace"/> is true then the "current" UmbracoContext is replaced
/// with a new one even if there is one already. See <see cref="WebRuntimeComponent"/>. Has to do with
+3 -12
View File
@@ -325,23 +325,14 @@ namespace Umbraco.Web
var umbracoType = Constants.UdiEntityType.ToUmbracoObjectType(udi.EntityType);
var entityService = Current.Services.EntityService;
switch (umbracoType)
{
case UmbracoObjectTypes.Document:
return Content(guidUdi.Guid);
case UmbracoObjectTypes.Media:
// fixme - need to implement Media(guid)!
var mediaAttempt = entityService.GetId(guidUdi.Guid, umbracoType);
if (mediaAttempt.Success)
return Media(mediaAttempt.Result);
break;
return Media(guidUdi.Guid);
case UmbracoObjectTypes.Member:
// fixme - need to implement Member(guid)!
var memberAttempt = entityService.GetId(guidUdi.Guid, umbracoType);
if (memberAttempt.Success)
return Member(memberAttempt.Result);
break;
return Member(guidUdi.Guid);
}
return null;
@@ -650,7 +641,7 @@ namespace Umbraco.Web
//TODO: This is horrible but until the media cache properly supports GUIDs we have no choice here and
// currently there won't be any way to add this method correctly to `ITypedPublishedContentQuery` without breaking an interface and adding GUID support for media
var entityService = Current.Services.EntityService; // fixme inject
var entityService = Current.Services.EntityService; // todo inject
var mediaAttempt = entityService.GetId(id, UmbracoObjectTypes.Media);
return mediaAttempt.Success ? ContentQuery.Media(mediaAttempt.Result) : null;
}
+1 -1
View File
@@ -23,7 +23,7 @@ namespace Umbraco.Web
UmbracoContext = umbracoContext;
Umbraco = new UmbracoHelper(umbracoContext, services);
// fixme inject somehow
// todo inject somehow
Logger = Current.Logger;
ProfilingLogger = Current.ProfilingLogger;
Services = Current.Services;
@@ -11,6 +11,6 @@ namespace Umbraco.Web.WebApi.Filters
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public sealed class EnableOverrideAuthorizationAttribute : Attribute
{
//fixme we should remove this and use the System.Web.Http.OverrideAuthorizationAttribute which uses IOverrideFilter instead
//todo we should remove this and use the System.Web.Http.OverrideAuthorizationAttribute which uses IOverrideFilter instead
}
}
@@ -83,13 +83,13 @@ namespace Umbraco.Web.WebApi.Filters
}
else if (Udi.TryParse(argument, true, out Udi udi))
{
//fixme: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services
//todo: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services
nodeId = Current.Services.EntityService.GetId(udi).Result;
}
else
{
Guid.TryParse(argument, out Guid key);
//fixme: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services
//todo: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services
nodeId = Current.Services.EntityService.GetId(key, UmbracoObjectTypes.Document).Result;
}
}
@@ -42,7 +42,7 @@ namespace Umbraco.Web.WebApi.Filters
_paramName = paramName;
}
// fixme v8 guess this is not used anymore, source is ignored?!
// todo v8 guess this is not used anymore, source is ignored?!
public EnsureUserPermissionForMediaAttribute(string paramName, DictionarySource source)
{
if (string.IsNullOrEmpty(paramName)) throw new ArgumentNullOrEmptyException(nameof(paramName));
@@ -5,7 +5,7 @@ using System.Web.Http.Controllers;
namespace Umbraco.Web.WebApi.Filters
{
//fixme remove this since we don't need it, see notes in EnableOverrideAuthorizationAttribute
//todo remove this since we don't need it, see notes in EnableOverrideAuthorizationAttribute
/// <summary>
/// Abstract auth filter class that can be used to enable overriding class auth filters at the action level
@@ -19,7 +19,7 @@ namespace Umbraco.Web.WebApi
/// </summary>
internal static bool Enable = true;
// fixme - inject!
// todo - inject!
private readonly UmbracoContext _umbracoContext;
private readonly IRuntimeState _runtimeState;