Merge pull request #7996 from umbraco/netcore/bugfix/logging-fixing

Fixes up all things logging in netcore
This commit is contained in:
Bjarke Berg
2020-04-22 12:13:10 +02:00
committed by GitHub
60 changed files with 882 additions and 341 deletions
+1
View File
@@ -176,3 +176,4 @@ build/temp/
/src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/lib/*
/src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/views/*
/src/Umbraco.Web.UI.NetCore/wwwroot/App_Data/TEMP/*
/src/Umbraco.Web.UI.NetCore/App_Data/Logs/*
@@ -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>
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Logging
{
public interface ILoggingConfiguration
{
/// <summary>
/// The physical path where logs are stored
/// </summary>
string LogDirectory { get; }
string LogConfigurationFile { get; }
string UserLogConfigurationFile { get; }
}
}
+1 -7
View File
@@ -2,18 +2,12 @@
namespace Umbraco.Core.Logging
{
/// <summary>
/// Defines the profiling service.
/// </summary>
public interface IProfiler
{
/// <summary>
/// Renders the profiling results.
/// </summary>
/// <returns>The profiling results.</returns>
/// <remarks>Generally used for HTML rendering.</remarks>
string Render();
/// <summary>
/// Gets an <see cref="IDisposable"/> that will time the code between its creation and disposal.
/// </summary>
+15
View File
@@ -0,0 +1,15 @@
namespace Umbraco.Core.Logging
{
/// <summary>
/// Used to render a profiler in a web page
/// </summary>
public interface IProfilerHtml
{
/// <summary>
/// Renders the profiling results.
/// </summary>
/// <returns>The profiling results.</returns>
/// <remarks>Generally used for HTML rendering.</remarks>
string Render();
}
}
-6
View File
@@ -15,12 +15,6 @@ namespace Umbraco.Core.Logging
_logger = logger;
}
/// <inheritdoc/>
public string Render()
{
return string.Empty;
}
/// <inheritdoc/>
public IDisposable Step(string name)
{
@@ -0,0 +1,20 @@
using System;
namespace Umbraco.Core.Logging
{
public class LoggingConfiguration : ILoggingConfiguration
{
public LoggingConfiguration(string logDirectory, string logConfigurationFile, string userLogConfigurationFile)
{
LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory));
LogConfigurationFile = logConfigurationFile ?? throw new ArgumentNullException(nameof(logConfigurationFile));
UserLogConfigurationFile = userLogConfigurationFile ?? throw new ArgumentNullException(nameof(userLogConfigurationFile));
}
public string LogDirectory { get; }
public string LogConfigurationFile { get; }
public string UserLogConfigurationFile { get; }
}
}
-5
View File
@@ -6,11 +6,6 @@ namespace Umbraco.Core.Logging
{
private readonly VoidDisposable _disposable = new VoidDisposable();
public string Render()
{
return string.Empty;
}
public IDisposable Step(string name)
{
return _disposable;
@@ -0,0 +1,7 @@
namespace Umbraco.Net
{
public class NullSessionIdResolver : ISessionIdResolver
{
public string SessionId => null;
}
}
@@ -13,8 +13,17 @@ namespace Umbraco.Core.Composing
/// <param name="builder"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
=> builder.UseUmbraco(new UmbracoServiceProviderFactory());
{
return builder
.UseUmbraco(new UmbracoServiceProviderFactory());
}
/// <summary>
/// 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, UmbracoServiceProviderFactory umbracoServiceProviderFactory)
=> builder.UseServiceProviderFactory(umbracoServiceProviderFactory);
}
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
namespace Umbraco.Core.Diagnostics
@@ -100,7 +101,7 @@ namespace Umbraco.Core.Diagnostics
return bRet;
}
public static bool Dump(IMarchal marchal, IIOHelper ioHelper, Option options = Option.WithFullMemory, bool withException = false)
public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false)
{
lock (LockO)
{
@@ -110,7 +111,7 @@ namespace Umbraco.Core.Diagnostics
// filter everywhere in our code = not!
var stacktrace = withException ? Environment.StackTrace : string.Empty;
var filepath = ioHelper.MapPath("~/App_Data/MiniDump");
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
if (Directory.Exists(filepath) == false)
Directory.CreateDirectory(filepath);
@@ -122,11 +123,11 @@ namespace Umbraco.Core.Diagnostics
}
}
public static bool OkToDump(IIOHelper ioHelper)
public static bool OkToDump(IHostingEnvironment hostingEnvironment)
{
lock (LockO)
{
var filepath = ioHelper.MapPath("~/App_Data/MiniDump");
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
if (Directory.Exists(filepath) == false) return true;
var count = Directory.GetFiles(filepath, "*.dmp").Length;
return count < 8;
@@ -16,14 +16,14 @@ namespace Umbraco.Core.Logging
// but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which
// means we cannot get rid of Serilog entirely. We may want to revisit this at some point.
// TODO: Do we still need this, is there a non-pre release package shipped?
private static readonly Lazy<global::Serilog.ILogger> MinimalLogger = new Lazy<global::Serilog.ILogger>(() => new LoggerConfiguration().CreateLogger());
public string Render(string messageTemplate, params object[] args)
{
// by default, unless initialized otherwise, Log.Logger is SilentLogger which cannot bind message
// templates. Log.Logger is set to a true Logger when initializing Umbraco's logger, but in case
// that has not been done already - use a temp minimal logger (eg for tests).
var logger = Log.Logger as global::Serilog.Core.Logger ?? MinimalLogger.Value;
// resolve a minimal logger instance which is used to bind message templates
var logger = MinimalLogger.Value;
var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties);
@@ -11,13 +11,13 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpRequestIdEnricher : ILogEventEnricher
public class HttpRequestIdEnricher : ILogEventEnricher
{
private readonly Func<IRequestCache> _requestCacheGetter;
private readonly IRequestCache _requestCache;
public HttpRequestIdEnricher(Func<IRequestCache> requestCacheGetter)
public HttpRequestIdEnricher(IRequestCache requestCache)
{
_requestCacheGetter = requestCacheGetter;
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
/// <summary>
@@ -34,11 +34,8 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var requestCache = _requestCacheGetter();
if(requestCache is null) return;
Guid requestId;
if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, requestCache))
if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache))
return;
var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId));
@@ -13,9 +13,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpRequestNumberEnricher : ILogEventEnricher
public class HttpRequestNumberEnricher : ILogEventEnricher
{
private readonly Func<IRequestCache> _requestCacheGetter;
private readonly IRequestCache _requestCache;
private static int _lastRequestNumber;
private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber";
@@ -25,9 +25,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
private const string _httpRequestNumberPropertyName = "HttpRequestNumber";
public HttpRequestNumberEnricher(Func<IRequestCache> requestCacheGetter)
public HttpRequestNumberEnricher(IRequestCache requestCache)
{
_requestCacheGetter = requestCacheGetter;
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
/// <summary>
@@ -39,10 +39,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var requestCache = _requestCacheGetter();
if (requestCache is null) return;
var requestNumber = requestCache.Get(_requestNumberItemName,
var requestNumber = _requestCache.Get(_requestNumberItemName,
() => Interlocked.Increment(ref _lastRequestNumber));
var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber));
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpSessionIdEnricher : ILogEventEnricher
public class HttpSessionIdEnricher : ILogEventEnricher
{
private readonly ISessionIdResolver _sessionIdResolver;
@@ -0,0 +1,96 @@
using System;
using System.Reflection;
using System.Threading;
using Serilog.Core;
using Serilog.Events;
using Umbraco.Core.Configuration;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Hosting;
namespace Umbraco.Infrastructure.Logging.Serilog.Enrichers
{
/// <summary>
/// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can
/// </summary>
public class ThreadAbortExceptionEnricher : ILogEventEnricher
{
private readonly ICoreDebugSettings _coreDebugSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IMarchal _marchal;
public ThreadAbortExceptionEnricher(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal)
{
_coreDebugSettings = coreDebugSettings;
_hostingEnvironment = hostingEnvironment;
_marchal = marchal;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
switch (logEvent.Level)
{
case LogEventLevel.Error:
case LogEventLevel.Fatal:
DumpThreadAborts(logEvent, propertyFactory);
break;
}
}
private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (!IsTimeoutThreadAbortException(logEvent.Exception)) return;
var message = "The thread has been aborted, because the request has timed out.";
// dump if configured, or if stacktrace contains Monitor.ReliableEnter
var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception);
// dump if it is ok to dump (might have a cap on number of dump...)
dump &= MiniDump.OkToDump(_hostingEnvironment);
if (!dump)
{
message += ". No minidump was created.";
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
else
try
{
var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true);
message += dumped
? ". A minidump was created in App_Data/MiniDump."
: ". Failed to create a minidump.";
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
catch (Exception ex)
{
message = "Failed to create a minidump. " + ex;
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
}
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false;
var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic);
if (timeoutField == null) return false;
return (bool)timeoutField.GetValue(abort.ExceptionState);
}
private static bool IsMonitorEnterThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
var stacktrace = abort.StackTrace;
return stacktrace.Contains("System.Threading.Monitor.ReliableEnter");
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
using Serilog;
using Serilog.Configuration;
@@ -6,11 +7,8 @@ using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Compact;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Net;
namespace Umbraco.Core.Logging.Serilog
{
@@ -24,27 +22,30 @@ namespace Umbraco.Core.Logging.Serilog
/// It is highly recommended that you keep/use this default in your own logging config customizations
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="hostingEnvironment"></param>
public static LoggerConfiguration MinimalConfiguration(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func<IRequestCache> requestCacheGetter)
/// <param name="IHostingEnvironment"></param>
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration MinimalConfiguration(
this LoggerConfiguration logConfig,
IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration)
{
global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg));
//Set this environment variable - so that it can be used in external config file
//add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" />
Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.ApplicationPhysicalPath, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only)
.Enrich.WithProcessId()
.Enrich.WithProcessName()
.Enrich.WithThreadId()
.Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
.Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
.Enrich.WithProperty("AppDomainAppId", hostingEnvironment.ApplicationId.ReplaceNonAlphanumericChars(string.Empty))
.Enrich.WithProperty("MachineName", Environment.MachineName)
.Enrich.With<Log4NetLevelMapperEnricher>()
.Enrich.With(new HttpSessionIdEnricher(sessionIdResolver))
.Enrich.With(new HttpRequestNumberEnricher(requestCacheGetter))
.Enrich.With(new HttpRequestIdEnricher(requestCacheGetter));
.Enrich.FromLogContext(); // allows us to dynamically enrich
return logConfig;
}
@@ -53,13 +54,14 @@ namespace Umbraco.Core.Logging.Serilog
/// Outputs a .txt format log at /App_Data/Logs/
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="loggingConfiguration"></param>
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
/// <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)
public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
//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",
logConfig.WriteTo.File(Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..txt"),
shared: true,
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: minimumLevel,
@@ -99,7 +101,8 @@ namespace Umbraco.Core.Logging.Serilog
rollingInterval,
rollOnFileSizeLimit,
retainedFileCountLimit,
encoding),
encoding,
null),
sinkMapCountLimit:0)
);
}
@@ -109,13 +112,14 @@ namespace Umbraco.Core.Logging.Serilog
/// Outputs a CLEF format JSON log at /App_Data/Logs/
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="loggingConfiguration"></param>
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
/// <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)
public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
//.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier)
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json",
logConfig.WriteTo.File(new CompactJsonFormatter(), Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..json"),
shared: true,
rollingInterval: RollingInterval.Day, //Create a new JSON file every day
retainedFileCountLimit: retainedFileCount, //Setting to null means we keep all files - default is 31 days
@@ -129,10 +133,11 @@ namespace Umbraco.Core.Logging.Serilog
/// That allows the main logging pipeline to be configured
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig)
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration)
{
//Read from main serilog.config file
logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config");
logConfig.ReadFrom.AppSettings(filePath: loggingConfiguration.LogConfigurationFile);
return logConfig;
}
@@ -142,13 +147,15 @@ namespace Umbraco.Core.Logging.Serilog
/// That allows a separate logging pipeline to be configured that will not affect the main Umbraco log
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig)
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration)
{
//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"));
cfg.ReadFrom.AppSettings(filePath: loggingConfiguration.UserLogConfigurationFile));
return logConfig;
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Infrastructure.Logging.Serilog.Enrichers;
namespace Umbraco.Infrastructure.Logging.Serilog
{
public class SerilogComposer : ICoreComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique<ThreadAbortExceptionEnricher>();
composition.RegisterUnique<HttpSessionIdEnricher>();
composition.RegisterUnique<HttpRequestNumberEnricher>();
composition.RegisterUnique<HttpRequestIdEnricher>();
}
}
}
@@ -1,72 +1,56 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using Serilog;
using Serilog.Events;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Net;
namespace Umbraco.Core.Logging.Serilog
{
///<summary>
/// Implements <see cref="ILogger"/> on top of Serilog.
///</summary>
public class SerilogLogger : ILogger, IDisposable
{
private readonly ICoreDebugSettings _coreDebugSettings;
private readonly IIOHelper _ioHelper;
private readonly IMarchal _marchal;
public global::Serilog.ILogger SerilogLog { get; }
/// <summary>
/// Initialize a new instance of the <see cref="SerilogLogger"/> class with a configuration file.
/// </summary>
/// <param name="logConfigFile"></param>
public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, FileInfo logConfigFile)
public SerilogLogger(FileInfo logConfigFile)
{
_coreDebugSettings = coreDebugSettings;
_ioHelper = ioHelper;
_marchal = marchal;
Log.Logger = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + logConfigFile)
SerilogLog = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: logConfigFile.FullName)
.CreateLogger();
}
public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, LoggerConfiguration logConfig)
public SerilogLogger(LoggerConfiguration logConfig)
{
_coreDebugSettings = coreDebugSettings;
_ioHelper = ioHelper;
_marchal = marchal;
//Configure Serilog static global logger with config passed in
Log.Logger = logConfig.CreateLogger();
SerilogLog = logConfig.CreateLogger();
}
/// <summary>
/// Creates a logger with some pre-defined configuration and remainder from config file
/// </summary>
/// <remarks>Used by UmbracoApplicationBase to get its logger.</remarks>
public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func<IRequestCache> requestCacheGetter, ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal)
public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration)
{
var loggerConfig = new LoggerConfiguration();
loggerConfig
.MinimalConfiguration(hostingEnvironment, sessionIdResolver, requestCacheGetter)
.ReadFromConfigFile()
.ReadFromUserConfigFile();
.MinimalConfiguration(hostingEnvironment, loggingConfiguration)
.ReadFromConfigFile(loggingConfiguration)
.ReadFromUserConfigFile(loggingConfiguration);
return new SerilogLogger(coreDebugSettings, ioHelper, marchal, loggerConfig);
return new SerilogLogger(loggerConfig);
}
/// <summary>
/// Gets a contextualized logger.
/// </summary>
private global::Serilog.ILogger LoggerFor(Type reporting)
=> Log.Logger.ForContext(reporting);
=> SerilogLog.ForContext(reporting);
/// <summary>
/// Maps Umbraco's log level to Serilog's.
@@ -99,8 +83,7 @@ namespace Umbraco.Core.Logging.Serilog
/// <inheritdoc/>
public void Fatal(Type reporting, Exception exception, string message)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message);
var logger = LoggerFor(reporting);
logger.Fatal(exception, message);
}
@@ -108,8 +91,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Fatal(Type reporting, Exception exception)
{
var logger = LoggerFor(reporting);
var message = "Exception.";
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message);
var message = "Exception.";
logger.Fatal(exception, message);
}
@@ -128,16 +110,14 @@ namespace Umbraco.Core.Logging.Serilog
/// <inheritdoc/>
public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate);
var logger = LoggerFor(reporting);
logger.Fatal(exception, messageTemplate, propertyValues);
}
/// <inheritdoc/>
public void Error(Type reporting, Exception exception, string message)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message);
var logger = LoggerFor(reporting);
logger.Error(exception, message);
}
@@ -146,7 +126,6 @@ namespace Umbraco.Core.Logging.Serilog
{
var logger = LoggerFor(reporting);
var message = "Exception";
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message);
logger.Error(exception, message);
}
@@ -166,67 +145,9 @@ namespace Umbraco.Core.Logging.Serilog
public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate);
logger.Error(exception, messageTemplate, propertyValues);
}
private void DumpThreadAborts(global::Serilog.ILogger logger, LogEventLevel level, Exception exception, ref string messageTemplate)
{
var dump = false;
if (IsTimeoutThreadAbortException(exception))
{
messageTemplate += "\r\nThe thread has been aborted, because the request has timed out.";
// dump if configured, or if stacktrace contains Monitor.ReliableEnter
dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception);
// dump if it is ok to dump (might have a cap on number of dump...)
dump &= MiniDump.OkToDump(_ioHelper);
}
if (dump)
{
try
{
var dumped = MiniDump.Dump(_marchal, _ioHelper, withException: true);
messageTemplate += dumped
? "\r\nA minidump was created in App_Data/MiniDump"
: "\r\nFailed to create a minidump";
}
catch (Exception ex)
{
messageTemplate += "\r\nFailed to create a minidump";
//Log a new entry (as opposed to appending to same log entry)
logger.Write(level, ex, "Failed to create a minidump ({ExType}: {ExMessage})",
new object[]{ ex.GetType().FullName, ex.Message });
}
}
}
private static bool IsMonitorEnterThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
var stacktrace = abort.StackTrace;
return stacktrace.Contains("System.Threading.Monitor.ReliableEnter");
}
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false;
var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic);
if (timeoutField == null) return false;
return (bool) timeoutField.GetValue(abort.ExceptionState);
}
/// <inheritdoc/>
public void Warn(Type reporting, string message)
{
@@ -289,7 +210,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Dispose()
{
Log.CloseAndFlush();
SerilogLog.DisposeIfDisposable();
}
}
}
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Logging.Viewer
{
@@ -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);
}
}
@@ -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<SerilogJsonLogViewer>();
}
}
}
@@ -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("[]");
}
}
}
}
@@ -5,22 +5,25 @@ 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
{
internal class JsonLogViewer : LogViewerSourceBase
internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase
{
private readonly string _logsPath;
private readonly ILogger _logger;
public JsonLogViewer(ILogger logger, IIOHelper ioHelper, string logsPath = "", string searchPath = "") : base(ioHelper, searchPath)
public SerilogJsonLogViewer(
ILogger logger,
ILogViewerConfig logViewerConfig,
ILoggingConfiguration loggingConfiguration,
global::Serilog.ILogger serilogLog)
: base(logViewerConfig, serilogLog)
{
if (string.IsNullOrEmpty(logsPath))
logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
_logsPath = logsPath;
_logger = logger;
_logsPath = loggingConfiguration.LogDirectory;
}
private const int FileSizeCap = 100;
@@ -62,9 +65,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 +74,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)
@@ -130,7 +130,7 @@ namespace Umbraco.Core.Logging.Viewer
{
// As we are reading/streaming one line at a time in the JSON file
// Thus we can not report the line number, as it will always be 1
_logger.Error<JsonLogViewer>(ex, "Unable to parse a line in the JSON log file");
_logger.Error<SerilogJsonLogViewer>(ex, "Unable to parse a line in the JSON log file");
evt = null;
return true;
@@ -1,31 +1,22 @@
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
public abstract class SerilogLogViewerSourceBase : ILogViewer
{
private readonly string _searchesConfigPath;
private readonly IIOHelper _ioHelper;
private readonly ILogViewerConfig _logViewerConfig;
private readonly global::Serilog.ILogger _serilogLog;
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 SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog)
{
_logViewerConfig = logViewerConfig;
_serilogLog = serilogLog;
}
public abstract bool CanHandleLargeLogs { get; }
@@ -38,55 +29,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)
{
@@ -101,7 +50,7 @@ namespace Umbraco.Core.Logging.Viewer
/// <returns></returns>
public string GetLogLevel()
{
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(Log.Logger.IsEnabled)?.Min() ?? null;
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(_serilogLog.IsEnabled)?.Min() ?? null;
return logLevel?.ToString() ?? "";
}
@@ -182,15 +131,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);
}
}
}
}
@@ -29,6 +29,7 @@ using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using Umbraco.Examine;
using Umbraco.Infrastructure.Media;
using Umbraco.Net;
using Umbraco.Web;
using Umbraco.Web.Actions;
using Umbraco.Web.Cache;
@@ -351,10 +352,6 @@ namespace Umbraco.Core.Runtime
// register accessors for cultures
composition.RegisterUnique<IDefaultCultureAccessor, DefaultCultureAccessor>();
}
}
}
@@ -152,5 +152,14 @@ namespace Umbraco.Tests.Common
return mock.Object;
}
public ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv = null)
{
hostingEnv = hostingEnv ?? GetHostingEnvironment();
return new LoggingConfiguration(
Path.Combine(hostingEnv.ApplicationPhysicalPath, "App_Data\\Logs"),
Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.config"),
Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.user.config"));
}
}
}
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Web.Common.AspNetCore;
namespace Umbraco.Tests.Integration.Implementations
@@ -101,7 +101,7 @@ namespace Umbraco.Tests.Integration
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, null, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -140,7 +140,7 @@ namespace Umbraco.Tests.Integration
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, null, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -108,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, null, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
+9 -3
View File
@@ -1,9 +1,11 @@
using Moq;
using NUnit.Framework;
using Serilog;
using System;
using System.IO;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Logging.Viewer;
using Umbraco.Tests.TestHelpers;
@@ -33,13 +35,16 @@ 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 loggingConfiguration = TestHelper.GetLoggingConfiguration(hostingEnv);
var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName);
_newLogfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"App_Data\Logs\");
_newLogfileDirPath = loggingConfiguration.LogDirectory;
_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 +56,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 SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, Log.Logger);
}
[OneTimeTearDown]
@@ -17,6 +17,7 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
using Umbraco.Core.Scoping;
using Umbraco.Net;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Stubs;
using Umbraco.Web;
@@ -135,6 +136,7 @@ namespace Umbraco.Tests.Runtimes
public override IFactory Configure(IRegister container)
{
container.Register<IApplicationShutdownRegistry, AspNetApplicationShutdownRegistry>(Lifetime.Singleton);
container.Register<ISessionIdResolver, NullSessionIdResolver>(Lifetime.Singleton);
var factory = base.Configure(container);
return factory;
@@ -19,11 +19,6 @@ namespace Umbraco.Tests.TestHelpers.Stubs
private static bool _enabled;
public string Render()
{
return string.Empty;
}
public IDisposable Step(string name)
{
return _enabled ? MiniProfiler.Current.Step(name) : null;
@@ -324,6 +324,8 @@ namespace Umbraco.Tests.TestHelpers
public static IHostingEnvironment GetHostingEnvironment() => _testHelperInternal.GetHostingEnvironment();
public static ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv) => _testHelperInternal.GetLoggingConfiguration(hostingEnv);
public static IApplicationShutdownRegistry GetHostingEnvironmentLifetime() => _testHelperInternal.GetHostingEnvironmentLifetime();
public static IIpResolver GetIpResolver() => _testHelperInternal.GetIpResolver();
+1 -1
View File
@@ -264,7 +264,7 @@ namespace Umbraco.Tests.Testing
profiler = Mock.Of<IProfiler>();
break;
case UmbracoTestOptions.Logger.Serilog:
logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
profiler = new LogProfiler(logger);
break;
case UmbracoTestOptions.Logger.Console:
@@ -18,7 +18,7 @@ namespace Umbraco.Tests.UmbracoExamine
public void InitializeFixture()
{
var logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
var logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
_profilingLogger = new ProfilingLogger(logger, new LogProfiler(logger));
}
@@ -1,9 +1,13 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Context;
using Smidge;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Infrastructure.Logging.Serilog.Enrichers;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.BackOffice.AspNetCore
{
@@ -31,6 +35,10 @@ namespace Umbraco.Web.BackOffice.AspNetCore
var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime);
hostLifetime.RegisterObject(runtimeShutdown);
// Register our global threadabort enricher for logging
var threadAbortEnricher = app.ApplicationServices.GetRequiredService<ThreadAbortExceptionEnricher>();
LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context
// Start the runtime!
runtime.Start();
@@ -64,6 +72,15 @@ namespace Umbraco.Web.BackOffice.AspNetCore
}
}
public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
app.UseMiddleware<UmbracoRequestLoggingMiddleware>();
return app;
}
public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
@@ -5,13 +5,12 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreHostingEnvironment : Umbraco.Core.Hosting.IHostingEnvironment
{
private readonly IHostingSettings _hostingSettings;
private readonly IWebHostEnvironment _webHostEnvironment;
@@ -28,6 +27,7 @@ namespace Umbraco.Web.Common.AspNetCore
ApplicationVirtualPath = "/"; //TODO how to find this, This is a server thing, not application thing.
IISVersion = new Version(0, 0); // TODO not necessary IIS
}
public bool IsHosted { get; } = true;
@@ -8,6 +8,10 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Extensions.Hosting;
using Serilog.Extensions.Logging;
using Smidge;
using Smidge.Nuglify;
using Umbraco.Composing;
@@ -73,13 +77,22 @@ namespace Umbraco.Web.Common.Extensions
var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer;
var loggingConfig = new LoggingConfiguration(
Path.Combine(webHostEnvironment.ContentRootPath, "App_Data\\Logs"),
Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.config"),
Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.user.config"));
IHttpContextAccessor httpContextAccessor = new HttpContextAccessor();
services.AddSingleton<IHttpContextAccessor>(httpContextAccessor);
var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items);
services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), requestCache, httpContextAccessor, out factory);
services.AddUmbracoCore(webHostEnvironment,
umbContainer,
Assembly.GetEntryAssembly(),
requestCache,
loggingConfig,
out factory);
return services;
}
@@ -92,6 +105,8 @@ namespace Umbraco.Web.Common.Extensions
/// <param name="umbContainer"></param>
/// <param name="entryAssembly"></param>
/// <param name="requestCache"></param>
/// <param name="httpContextAccessor"></param>
/// <param name="loggingConfiguration"></param>
/// <param name="factory"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoCore(
@@ -100,7 +115,7 @@ namespace Umbraco.Web.Common.Extensions
IRegister umbContainer,
Assembly entryAssembly,
IRequestCache requestCache,
IHttpContextAccessor httpContextAccessor,
ILoggingConfiguration loggingConfiguration,
out IFactory factory)
{
if (services is null) throw new ArgumentNullException(nameof(services));
@@ -112,7 +127,11 @@ namespace Umbraco.Web.Common.Extensions
var configs = serviceProvider.GetService<Configs>();
CreateCompositionRoot(services, configs, httpContextAccessor, webHostEnvironment, out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
CreateCompositionRoot(services,
configs,
webHostEnvironment,
loggingConfiguration,
out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
var globalSettings = configs.Global();
var umbracoVersion = new UmbracoVersion(globalSettings);
@@ -133,7 +152,7 @@ namespace Umbraco.Web.Common.Extensions
return services;
}
private static ITypeFinder CreateTypeFinder(ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly)
private static ITypeFinder CreateTypeFinder(Core.Logging.ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly)
{
// TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however
// this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now.
@@ -144,7 +163,7 @@ namespace Umbraco.Web.Common.Extensions
}
private static IRuntime GetCoreRuntime(
Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger,
Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger,
IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo,
ITypeFinder typeFinder, IRequestCache requestCache)
{
@@ -180,31 +199,65 @@ namespace Umbraco.Web.Common.Extensions
return coreRuntime;
}
private static IServiceCollection CreateCompositionRoot(IServiceCollection services, Configs configs, IHttpContextAccessor httpContextAccessor, IWebHostEnvironment webHostEnvironment,
out ILogger logger, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment,
out IBackOfficeInfo backOfficeInfo, out IProfiler profiler)
private static IServiceCollection CreateCompositionRoot(
IServiceCollection services,
Configs configs,
IWebHostEnvironment webHostEnvironment,
ILoggingConfiguration loggingConfiguration,
out Core.Logging.ILogger logger,
out IIOHelper ioHelper,
out Core.Hosting.IHostingEnvironment hostingEnvironment,
out IBackOfficeInfo backOfficeInfo,
out IProfiler profiler)
{
if (configs == null)
throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}");
var hostingSettings = configs.Hosting();
var coreDebug = configs.CoreDebug();
var globalSettings = configs.Global();
hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment);
ioHelper = new IOHelper(hostingEnvironment, globalSettings);
logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment,
new AspNetCoreSessionManager(httpContextAccessor),
// TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
() => services.BuildServiceProvider().GetService<IRequestCache>(), coreDebug, ioHelper,
new AspNetCoreMarchal());
logger = AddLogger(services, hostingEnvironment, loggingConfiguration);
backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings);
profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor);
profiler = GetWebProfiler(hostingEnvironment);
return services;
}
/// <summary>
/// Create and configure the logger
/// </summary>
/// <param name="hostingEnvironment"></param>
private static Core.Logging.ILogger AddLogger(IServiceCollection services, Core.Hosting.IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration)
{
// Create a serilog logger
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration);
// Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since
// we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which
// is the only other option that these ext methods allow.
// I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that.
// Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs
services.AddSingleton<ILoggerFactory>(services => new SerilogLoggerFactory(logger.SerilogLog, false));
// This won't (and shouldn't) take ownership of the logger.
services.AddSingleton(logger.SerilogLog);
// Registered to provide two services...
var diagnosticContext = new DiagnosticContext(logger.SerilogLog);
// Consumed by e.g. middleware
services.AddSingleton(diagnosticContext);
// Consumed by user code
services.AddSingleton<IDiagnosticContext>(diagnosticContext);
return logger;
}
public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services,
IConfiguration configuration)
{
@@ -214,7 +267,7 @@ namespace Umbraco.Web.Common.Extensions
return services;
}
private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccessor)
private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment)
{
// create and start asap to profile boot
if (!hostingEnvironment.IsDebugMode)
@@ -224,7 +277,7 @@ namespace Umbraco.Web.Common.Extensions
return new VoidProfiler();
}
var webProfiler = new WebProfiler(httpContextAccessor);
var webProfiler = new WebProfiler();
webProfiler.StartBoot();
return webProfiler;
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Serilog.Context;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Net;
namespace Umbraco.Web.Common.Middleware
{
public class UmbracoRequestLoggingMiddleware
{
readonly RequestDelegate _next;
private readonly HttpSessionIdEnricher _sessionIdEnricher;
private readonly HttpRequestNumberEnricher _requestNumberEnricher;
private readonly HttpRequestIdEnricher _requestIdEnricher;
public UmbracoRequestLoggingMiddleware(RequestDelegate next,
HttpSessionIdEnricher sessionIdEnricher,
HttpRequestNumberEnricher requestNumberEnricher,
HttpRequestIdEnricher requestIdEnricher)
{
_next = next;
_sessionIdEnricher = sessionIdEnricher;
_requestNumberEnricher = requestNumberEnricher;
_requestIdEnricher = requestIdEnricher;
}
public async Task Invoke(HttpContext httpContext)
{
// TODO: Need to decide if we want this stuff still, there's new request logging in serilog:
// https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this?
using (LogContext.Push(_sessionIdEnricher))
using (LogContext.Push(_requestNumberEnricher))
using (LogContext.Push(_requestIdEnricher))
{
await _next(httpContext);
}
}
}
}
@@ -22,5 +22,4 @@ namespace Umbraco.Web.Common.Middleware
_umbracoRequestLifetimeManager.EndRequest(context);
}
}
}
@@ -7,6 +7,9 @@ using Umbraco.Core.Runtime;
using Umbraco.Core.Security;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Lifetime;
using Umbraco.Core.Diagnostics;
using Umbraco.Web.Common.Runtime.Profiler;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Common.Runtime
{
@@ -42,6 +45,10 @@ namespace Umbraco.Web.Common.Runtime
composition.RegisterUnique<ICookieManager, AspNetCoreCookieManager>();
composition.RegisterMultipleUnique<ISessionIdResolver, ISessionManager, AspNetCoreSessionManager>();
composition.RegisterUnique<IMarchal, AspNetCoreMarchal>();
composition.RegisterUnique<IProfilerHtml, WebProfilerHtml>();
}
}
}
@@ -1,61 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using StackExchange.Profiling;
using StackExchange.Profiling.Internal;
using Umbraco.Core;
using Umbraco.Core.Logging;
// TODO: This namespace is strange, not sure why i has "Runtime" in the name?
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebProfiler : IProfiler
{
private MiniProfiler _startupProfiler;
private readonly IHttpContextAccessor _httpContextAccessor;
private int _first;
public WebProfiler(IHttpContextAccessor httpContextAccessor)
{
// create our own provider, which can provide a profiler even during boot
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
///
/// </summary>
/// <remarks>
/// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work.
/// We fake the requestIds from the RequestState here.
/// </remarks>
public string Render()
{
var profiler = MiniProfiler.Current;
if (profiler == null) return string.Empty;
var context = _httpContextAccessor.HttpContext;
var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash();
var result = StackExchange.Profiling.Internal.Render.Includes(
profiler,
path: context.Request.PathBase + path,
isAuthorized: true,
requestIDs: new List<Guid>{ profiler.Id },
position: RenderPosition.Right,
showTrivial: profiler.Options.PopupShowTrivial,
showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren,
maxTracesToShow: profiler.Options.PopupMaxTracesToShow,
showControls:profiler.Options.ShowControls,
startHidden: profiler.Options.PopupStartHidden);
return result;
}
public IDisposable Step(string name)
{
return MiniProfiler.Current?.Step(name);
@@ -1,8 +1,15 @@
using Umbraco.Core.Composing;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Runtime.Profiler
{
internal class WebProfilerComposer : ComponentComposer<WebProfilerComponent>, ICoreComposer
{
public override void Compose(Composition composition)
{
base.Compose(composition);
composition.RegisterUnique<WebProfilerHtml>();
}
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using StackExchange.Profiling;
using StackExchange.Profiling.Internal;
using Umbraco.Core.Logging;
// TODO: This namespace is strange, not sure why i has "Runtime" in the name?
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebProfilerHtml : IProfilerHtml
{
private readonly IHttpContextAccessor _httpContextAccessor;
public WebProfilerHtml(IHttpContextAccessor httpContextAccessor)
{
// create our own provider, which can provide a profiler even during boot
_httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
/// <remarks>
/// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work.
/// We fake the requestIds from the RequestState here.
/// </remarks>
public string Render()
{
var profiler = MiniProfiler.Current;
if (profiler == null) return string.Empty;
var context = _httpContextAccessor.HttpContext;
var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash();
var result = StackExchange.Profiling.Internal.Render.Includes(
profiler,
path: context.Request.PathBase + path,
isAuthorized: true,
requestIDs: new List<Guid> { profiler.Id },
position: RenderPosition.Right,
showTrivial: profiler.Options.PopupShowTrivial,
showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren,
maxTracesToShow: profiler.Options.PopupMaxTracesToShow,
showControls: profiler.Options.ShowControls,
startHidden: profiler.Options.PopupStartHidden);
return result;
}
}
}
@@ -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>
@@ -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%'"
}
]
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Used to toggle the log levels for the main Umbraco log files -->
<!-- Found at /app_data/logs/ -->
<!-- NOTE: Changing this will also flow down into serilog.user.config -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- NOTE: This is how sources can have a different level -->
<!--
<add key="serilog:minimum-level:override:Umbraco.Core.Composing.TypeLoader" value="Warning" />
-->
<!-- NOTE: Only one logger below can be enabled, you cannot log JSON & TXT files at the same time -->
<!-- Default JSON log file -->
<!-- This is used by the default log viewer in the Umbraco backoffice -->
<add key="serilog:using:File" value="Umbraco.Infrastructure" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" />
<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> <!-- Number of log files to keep (or remove value to keep all files) -->
<add key="serilog:write-to:File.rollingInterval" value="Day" /> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!-- Optional TXT log file -->
<!--<add key="serilog:using:File" value="Serilog.Sinks.File" /> -->
<!--<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.shared" value="true" /> -->
<!--<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" /> -->
<!--<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> --> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" /> --> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- To write to new log locations (aka Sinks) such as your own .txt files with filtering, ELMAH.io, Elastic, SEQ -->
<!-- Please use the serilog.user.config file to configure your own logging needs -->
</appSettings>
</configuration>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Used to toggle the log levels for the main Umbraco log files -->
<!-- Found at /app_data/logs/ -->
<!-- NOTE: Changing this will also flow down into serilog.user.config -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- NOTE: This is how sources can have a different level -->
<!--
<add key="serilog:minimum-level:override:Umbraco.Core.Composing.TypeLoader" value="Warning" />
-->
<!-- NOTE: Only one logger below can be enabled, you cannot log JSON & TXT files at the same time -->
<!-- Default JSON log file -->
<!-- This is used by the default log viewer in the Umbraco backoffice -->
<add key="serilog:using:File" value="Umbraco.Infrastructure" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" />
<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> <!-- Number of log files to keep (or remove value to keep all files) -->
<add key="serilog:write-to:File.rollingInterval" value="Day" /> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!-- Optional TXT log file -->
<!--<add key="serilog:using:File" value="Serilog.Sinks.File" /> -->
<!--<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.shared" value="true" /> -->
<!--<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" /> -->
<!--<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> --> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" /> --> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- To write to new log locations (aka Sinks) such as your own .txt files with filtering, ELMAH.io, Elastic, SEQ -->
<!-- Please use the serilog.user.config file to configure your own logging needs -->
</appSettings>
</configuration>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Controls log levels for all user-defined child sub-logger sinks configured here (Set this higher than child sinks defined here) -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- For Different Namespaces - Set different logging levels -->
<!--
<add key="serilog:minimum-level:override:Microsoft" value="Warning" />
<add key="serilog:minimum-level:override:Microsoft.AspNetCore.Mvc" value="Error" />
<add key="serilog:minimum-level:override:YourNameSpace" value="Information" />
-->
<!-- All logs defined via user.config will contain this property (won't be in main Umbraco logs) -->
<!--
<add key="serilog:enrich:with-property:websiteName" value="My Awesome Website - Development" />
-->
<!-- Write to a user log file -->
<!--
<add key="serilog:using:File" value="Serilog.Sinks.File" />
<add key="serilog:write-to:File.path" value="%BASEDIR%\logs\my-custom-logfile.txt" />
<add key="serilog:write-to:File.shared" value="true" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="32" />--> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" />--> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- Filters all above sink's to use this expression -->
<!-- Common use case is to include SourceType starting with your own namespace -->
<!--
<add key="serilog:using:FilterExpressions" value="Serilog.Filters.Expressions" />
<add key="serilog:filter:ByIncludingOnly.expression" value="StartsWith(SourceContext, 'Umbraco.Core')" />
-->
</appSettings>
</configuration>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Controls log levels for all user-defined child sub-logger sinks configured here (Set this higher than child sinks defined here) -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- For Different Namespaces - Set different logging levels -->
<!--
<add key="serilog:minimum-level:override:Microsoft" value="Warning" />
<add key="serilog:minimum-level:override:Microsoft.AspNetCore.Mvc" value="Error" />
<add key="serilog:minimum-level:override:YourNameSpace" value="Information" />
-->
<!-- All logs defined via user.config will contain this property (won't be in main Umbraco logs) -->
<!--
<add key="serilog:enrich:with-property:websiteName" value="My Awesome Website - Development" />
-->
<!-- Write to a user log file -->
<!--
<add key="serilog:using:File" value="Serilog.Sinks.File" />
<add key="serilog:write-to:File.path" value="%BASEDIR%\logs\my-custom-logfile.txt" />
<add key="serilog:write-to:File.shared" value="true" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="32" />--> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" />--> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- Filters all above sink's to use this expression -->
<!-- Common use case is to include SourceType starting with your own namespace -->
<!--
<add key="serilog:using:FilterExpressions" value="Serilog.Filters.Expressions" />
<add key="serilog:filter:ByIncludingOnly.expression" value="StartsWith(SourceContext, 'Umbraco.Core')" />
-->
</appSettings>
</configuration>
-2
View File
@@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Umbraco.Core.Composing;
namespace Umbraco.Web.UI.BackOffice
+4 -1
View File
@@ -54,6 +54,7 @@ namespace Umbraco.Web.UI.BackOffice
});
//Finally initialize Current
// TODO: This should be moved to the UmbracoServiceProviderFactory when the container is cross-wired and then don't use the overload above to `out var factory`
Current.Initialize(
factory.GetInstance<ILogger> (),
factory.GetInstance<Configs>(),
@@ -77,6 +78,7 @@ namespace Umbraco.Web.UI.BackOffice
}
app.UseStatusCodePages();
app.UseUmbracoCore();
app.UseUmbracoRequestLogging();
app.UseUmbracoWebsite();
app.UseUmbracoBackOffice();
app.UseRouting();
@@ -90,7 +92,8 @@ namespace Umbraco.Web.UI.BackOffice
});
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync($"<html><body>Hello World!{Current.Profiler.Render()}</body></html>");
var profilerHtml = app.ApplicationServices.GetRequiredService<IProfilerHtml>();
await context.Response.WriteAsync($"<html><body>Hello World!{profilerHtml.Render()}</body></html>");
});
});
}
@@ -35,4 +35,37 @@
<Content Remove="App_Data\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="Config\serilog.Release.config" />
<Content Remove="Config\serilog.user.Release.config" />
</ItemGroup>
<ItemGroup>
<None Remove="Config\logviewer.searches.config.js" />
</ItemGroup>
<ItemGroup>
<Content Include="Config\logviewer.searches.config.js" />
</ItemGroup>
<ItemGroup>
<None Include="Config\serilog.Release.config">
<SubType>Designer</SubType>
<DependentUpon>serilog.config</DependentUpon>
</None>
<None Include="Config\serilog.user.Release.config">
<SubType>Designer</SubType>
<DependentUpon>serilog.user.config</DependentUpon>
</None>
</ItemGroup>
<ItemGroup>
<Content Update="Config\serilog.config">
<SubType>Designer</SubType>
</Content>
<Content Update="Config\serilog.user.config">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
</Project>
@@ -19,7 +19,7 @@
<!-- This is used by the default log viewer in the Umbraco backoffice -->
<add key="serilog:using:File" value="Umbraco.Infrastructure" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" />
<add key="serilog:write-to:File.path" value="%BASEDIR%\App_Data\Logs\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> <!-- Number of log files to keep (or remove value to keep all files) -->
<add key="serilog:write-to:File.rollingInterval" value="Day" /> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
@@ -27,7 +27,7 @@
<!-- Optional TXT log file -->
<!--<add key="serilog:using:File" value="Serilog.Sinks.File" /> -->
<!--<add key="serilog:write-to:File.path" value="%BASEDIR%\App_Data\Logs\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.shared" value="true" /> -->
<!--<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" /> -->
<!--<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> --> <!-- Number of log files to keep (or remove value to keep all files) -->
+2
View File
@@ -248,6 +248,8 @@ namespace Umbraco.Web.Composing
public static IProfiler Profiler => Factory.GetInstance<IProfiler>();
public static IProfilerHtml ProfilerHtml => Factory.GetInstance<IProfilerHtml>();
public static IProfilingLogger ProfilingLogger => Factory.GetInstance<IProfilingLogger>();
public static AppCaches AppCaches => Factory.GetInstance<AppCaches>();
@@ -30,7 +30,7 @@ namespace Umbraco.Web
/// <returns></returns>
public static IHtmlString RenderProfiler(this HtmlHelper helper)
{
return new HtmlString(Current.Profiler.Render());
return new HtmlString(Current.ProfilerHtml.Render());
}
/// <summary>
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Umbraco.Web.Logging
/// <remarks>
/// Profiling only runs when the app is in debug mode, see WebRuntime for how this gets created
/// </remarks>
internal class WebProfiler : IProfiler
internal class WebProfiler : IProfiler, IProfilerHtml
{
private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest";
private readonly WebProfilerProvider _provider;
@@ -25,6 +25,9 @@ using Umbraco.Web.Trees;
using Umbraco.Web.WebApi;
using Umbraco.Net;
using Umbraco.Web.AspNet;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Logging;
using Umbraco.Web.Logging;
namespace Umbraco.Web.Runtime
{
@@ -50,7 +53,8 @@ namespace Umbraco.Web.Runtime
composition.Register<IFilePermissionHelper, FilePermissionHelper>(Lifetime.Singleton);
composition.RegisterUnique<IMarchal, FrameworkMarchal>();
composition.RegisterUnique<IProfilerHtml, WebProfiler>();
composition.ComposeWebMappingProfiles();
+15 -3
View File
@@ -1,4 +1,5 @@
using System;
using Serilog.Context;
using System;
using System.IO;
using System.Reflection;
using System.Threading;
@@ -12,6 +13,8 @@ using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Logging.Serilog;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Net;
using Umbraco.Web.AspNet;
using Umbraco.Web.Hosting;
using Umbraco.Web.Logging;
@@ -34,12 +37,16 @@ namespace Umbraco.Web
var configFactory = new ConfigsFactory();
var hostingSettings = configFactory.HostingSettings;
var coreDebug = configFactory.CoreDebugSettings;
var globalSettings = configFactory.GlobalSettings;
var hostingEnvironment = new AspNetHostingEnvironment(hostingSettings);
var loggingConfiguration = new LoggingConfiguration(
Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data\\Logs"),
Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "config\\serilog.config"),
Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "config\\serilog.user.config"));
var ioHelper = new IOHelper(hostingEnvironment, globalSettings);
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, new AspNetSessionManager(), () => _factory?.GetInstance<IRequestCache>(), coreDebug, ioHelper, new FrameworkMarchal());
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration);
var configs = configFactory.Create();
var backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, ioHelper, logger, configFactory.WebRoutingSettings);
@@ -168,6 +175,11 @@ namespace Umbraco.Web
Umbraco.Composing.Current.BackOfficeInfo);
_factory = Current.Factory = _runtime.Configure(register);
// now we can add our request based logging enrichers (globally, which is what we were doing in netframework before)
LogContext.Push(new HttpSessionIdEnricher(_factory.GetInstance<ISessionIdResolver>()));
LogContext.Push(new HttpRequestNumberEnricher(_factory.GetInstance<IRequestCache>()));
LogContext.Push(new HttpRequestIdEnricher(_factory.GetInstance<IRequestCache>()));
_runtime.Start();
}