Fixes up usage of AppDomain.CurrentDomain.BaseDirectory in log viewer which cannot work in netcore since this is inconsistent with net framework, adds notes, starts configuring serilog with UseUmbraco

This commit is contained in:
Shannon
2020-04-21 13:07:20 +10:00
parent 9335f39495
commit ee37d3aa7a
15 changed files with 229 additions and 90 deletions

View File

@@ -6,6 +6,10 @@ namespace Umbraco.Core.Hosting
{
string SiteName { get; }
string ApplicationId { get; }
/// <summary>
/// Will return the physical path to the root of the application
/// </summary>
string ApplicationPhysicalPath { get; }
string LocalTempPath { get; }
@@ -27,10 +31,22 @@ namespace Umbraco.Core.Hosting
bool IsHosted { get; }
Version IISVersion { get; }
// TODO: Should we change this name to MapPathWebRoot ? and also have a new MapPathContentRoot ?
/// <summary>
/// Maps a virtual path to a physical path to the application's web root
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <remarks>
/// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however
/// in netcore the web root is /www therefore this will Map to a physical path within www.
/// </remarks>
string MapPath(string path);
/// <summary>
/// Maps a virtual path to the application's web root
/// Converts a virtual path to an absolute URL path based on the application's web root
/// </summary>
/// <param name="virtualPath">The virtual path. Must start with either ~/ or / else an exception is thrown.</param>
/// <returns></returns>

View File

@@ -11,10 +11,8 @@ namespace Umbraco.Core.Composing
/// Assigns a custom service provider factory to use Umbraco's container
/// </summary>
/// <param name="builder"></param>
/// <param name="umbracoServiceProviderFactory"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
=> builder.UseUmbraco(new UmbracoServiceProviderFactory());
public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory)
=> builder.UseServiceProviderFactory(umbracoServiceProviderFactory);
}

View File

@@ -29,6 +29,8 @@ namespace Umbraco.Core.Logging.Serilog
{
global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg));
// TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework
//Set this environment variable - so that it can be used in external config file
//add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" />
Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process);
@@ -57,6 +59,8 @@ namespace Umbraco.Core.Logging.Serilog
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
// TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework
//Main .txt logfile - in similar format to older Log4Net output
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File($@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt",
@@ -85,6 +89,8 @@ namespace Umbraco.Core.Logging.Serilog
Encoding encoding = null
)
{
// TODO: Deal with this method call since it's obsolete, we need to change this
return configuration.Async(
asyncConfiguration => asyncConfiguration.Map(AppDomainId, (_,mapConfiguration) =>
mapConfiguration.File(
@@ -113,6 +119,8 @@ namespace Umbraco.Core.Logging.Serilog
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
// TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework
//.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier)
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json",
@@ -131,6 +139,8 @@ namespace Umbraco.Core.Logging.Serilog
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig)
{
// TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework
//Read from main serilog.config file
logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config");
@@ -144,6 +154,8 @@ namespace Umbraco.Core.Logging.Serilog
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig)
{
// TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework
//A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above
logConfig.WriteTo.Logger(cfg =>
cfg.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.user.config"));

View File

@@ -32,6 +32,8 @@ namespace Umbraco.Core.Logging.Serilog
_ioHelper = ioHelper;
_marchal = marchal;
// TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework
Log.Logger = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + logConfigFile)
.CreateLogger();

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Logging.Viewer
{
public interface ILogViewerConfig
{
IReadOnlyList<SavedLogSearch> GetSavedSearches();
IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query);
IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query);
}
}

View File

@@ -5,6 +5,7 @@ using System.Linq;
using Newtonsoft.Json;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
namespace Umbraco.Core.Logging.Viewer
@@ -13,14 +14,19 @@ namespace Umbraco.Core.Logging.Viewer
{
private readonly string _logsPath;
private readonly ILogger _logger;
private readonly IHostingEnvironment _hostingEnvironment;
public JsonLogViewer(ILogger logger, IIOHelper ioHelper, string logsPath = "", string searchPath = "") : base(ioHelper, searchPath)
public JsonLogViewer(ILogger logger, ILogViewerConfig logViewerConfig, IHostingEnvironment hostingEnvironment) : base(logViewerConfig)
{
if (string.IsNullOrEmpty(logsPath))
logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
_logsPath = logsPath;
_hostingEnvironment = hostingEnvironment;
_logger = logger;
// TODO: this path is hard coded but it can actually be configured, but that is done via Serilog and we don't have a different abstraction/config
// for the logging path. We could make that, but then how would we get that abstraction into the Serilog config? I'm sure there is a way but
// don't have time right now to resolve that (since this was hard coded before). We could have a single/simple ILogConfig for umbraco that purely specifies
// the logging path and then we can have a special token that we replace in the serilog config that maps to that location? then at least we could inject
// that config in places where we are hard coding this path.
_logsPath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, @"App_Data\Logs\");
}
private const int FileSizeCap = 100;
@@ -62,9 +68,6 @@ namespace Umbraco.Core.Logging.Viewer
{
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
@@ -74,7 +77,7 @@ namespace Umbraco.Core.Logging.Viewer
//Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);
var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind);
//Foreach file we find - open it
foreach (var filePath in filesForCurrentDay)

View File

@@ -9,7 +9,8 @@ namespace Umbraco.Core.Logging.Viewer
{
public void Compose(Composition composition)
{
composition.SetLogViewer(factory => new JsonLogViewer(composition.Logger, factory.GetInstance<IIOHelper>()));
composition.RegisterUnique<ILogViewerConfig, LogViewerConfig>();
composition.SetLogViewer<JsonLogViewer>();
}
}
}

View File

@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Hosting;
using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
public class LogViewerConfig : ILogViewerConfig
{
private readonly IHostingEnvironment _hostingEnvironment;
private const string _pathToSearches = "~/Config/logviewer.searches.config.js";
private readonly FileInfo _searchesConfig;
public LogViewerConfig(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
var trimmedPath = _pathToSearches.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar);
var absolutePath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, trimmedPath);
_searchesConfig = new FileInfo(absolutePath);
}
public IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists();
var rawJson = System.IO.File.ReadAllText(_searchesConfig.FullName);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
public 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();
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfig.FullName, 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 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(_searchesConfig.FullName, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
return searches;
}
private void EnsureFileExists()
{
if (_searchesConfig.Exists) return;
using (var writer = _searchesConfig.CreateText())
{
writer.Write("[]");
}
}
}
}

View File

@@ -1,31 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
public abstract class LogViewerSourceBase : ILogViewer
{
private readonly string _searchesConfigPath;
private readonly IIOHelper _ioHelper;
private readonly ILogViewerConfig _logViewerConfig;
protected LogViewerSourceBase(IIOHelper ioHelper, string pathToSearches = "")
{
if (string.IsNullOrEmpty(pathToSearches))
// ReSharper disable once StringLiteralTypo
pathToSearches = ioHelper.MapPath("~/Config/logviewer.searches.config.js");
_searchesConfigPath = pathToSearches;
_ioHelper = ioHelper;
protected LogViewerSourceBase(ILogViewerConfig logViewerConfig)
{
_logViewerConfig = logViewerConfig;
}
public abstract bool CanHandleLargeLogs { get; }
@@ -38,55 +27,13 @@ namespace Umbraco.Core.Logging.Viewer
public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
public virtual IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]", _ioHelper);
var rawJson = System.IO.File.ReadAllText(_searchesConfigPath);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
=> _logViewerConfig.GetSavedSearches();
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, "[]", _ioHelper);
//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;
}
=> _logViewerConfig.AddSavedSearch(name, query);
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;
}
=> _logViewerConfig.DeleteSavedSearch(name, query);
public int GetNumberOfErrors(LogTimePeriod logTimePeriod)
{
@@ -182,15 +129,6 @@ namespace Umbraco.Core.Logging.Viewer
};
}
private static void EnsureFileExists(string path, string contents, IIOHelper ioHelper)
{
var absolutePath = ioHelper.MapPath(path);
if (System.IO.File.Exists(absolutePath)) return;
using (var writer = System.IO.File.CreateText(absolutePath))
{
writer.Write(contents);
}
}
}
}

View File

@@ -33,13 +33,14 @@ namespace Umbraco.Tests.Logging
//Create an example JSON log file to check results
//As a one time setup for all tets in this class/fixture
var ioHelper = TestHelper.IOHelper;
var hostingEnv = TestHelper.GetHostingEnvironment();
var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName);
_newLogfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"App_Data\Logs\");
_newLogfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"App_Data\Logs\");
_newLogfilePath = Path.Combine(_newLogfileDirPath, _logfileName);
var exampleSearchfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _searchfileName);
_newSearchfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Config\");
_newSearchfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"Config\");
_newSearchfilePath = Path.Combine(_newSearchfileDirPath, _searchfileName);
//Create/ensure Directory exists
@@ -51,7 +52,8 @@ namespace Umbraco.Tests.Logging
File.Copy(exampleSearchfilePath, _newSearchfilePath, true);
var logger = Mock.Of<Core.Logging.ILogger>();
_logViewer = new JsonLogViewer(logger, ioHelper, logsPath: _newLogfileDirPath, searchPath: _newSearchfilePath);
var logViewerConfig = new LogViewerConfig(hostingEnv);
_logViewer = new JsonLogViewer(logger, logViewerConfig, hostingEnv);
}
[OneTimeTearDown]

View File

@@ -0,0 +1,21 @@
using Microsoft.Extensions.Hosting;
using Serilog;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Extensions
{
public static class HostBuilderExtensions
{
/// <summary>
/// Assigns a custom service provider factory to use Umbraco's container
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
{
return builder
.UseSerilog()
.UseUmbraco(new UmbracoServiceProviderFactory());
}
}
}

View File

@@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Smidge" Version="3.1.0" />
<PackageReference Include="Smidge.Nuglify" Version="2.0.0" />
</ItemGroup>

View File

@@ -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 & Fatal 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%'"
}
]

View File

@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Umbraco.Core.Composing;
using Umbraco.Web.Common.Extensions;
namespace Umbraco.Web.UI.BackOffice
{

View File

@@ -35,4 +35,12 @@
<Content Remove="App_Data\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Config\logviewer.searches.config.js" />
</ItemGroup>
<ItemGroup>
<Content Include="Config\logviewer.searches.config.js" />
</ItemGroup>
</Project>