diff --git a/.gitignore b/.gitignore index 5f2432313f..d8c3f27d5a 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs new file mode 100644 index 0000000000..193235ca7e --- /dev/null +++ b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Cache +{ + /// + /// Implements a fast on top of HttpContext.Items. + /// + /// + /// If no current HttpContext items can be found (no current HttpContext, + /// or no Items...) then this cache acts as a pass-through and does not cache + /// anything. + /// + public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache + { + /// + /// Initializes a new instance of the class with a context, for unit tests! + /// + public GenericDictionaryRequestAppCache(Func> requestItems) : base() + { + ContextItems = requestItems; + } + + private Func> ContextItems { get; } + + public bool IsAvailable => TryGetContextItems(out _); + + private bool TryGetContextItems(out IDictionary items) + { + items = ContextItems?.Invoke(); + return items != null; + } + + /// + public override object Get(string key, Func factory) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return factory(); + + key = GetCacheKey(key); + + Lazy result; + + try + { + EnterWriteLock(); + result = items[key] as Lazy; // null if key not found + + // cannot create value within the lock, so if result.IsValueCreated is false, just + // do nothing here - means that if creation throws, a race condition could cause + // more than one thread to reach the return statement below and throw - accepted. + + if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null + { + result = SafeLazy.GetSafeLazy(factory); + items[key] = result; + } + } + finally + { + ExitWriteLock(); + } + + // using GetSafeLazy and GetSafeLazyValue ensures that we don't cache + // exceptions (but try again and again) and silently eat them - however at + // some point we have to report them - so need to re-throw here + + // this does not throw anymore + //return result.Value; + + var value = result.Value; // will not throw (safe lazy) + if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once! + return value; + } + + public bool Set(string key, object value) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return false; + key = GetCacheKey(key); + try + { + + EnterWriteLock(); + items[key] = SafeLazy.GetSafeLazy(() => value); + } + finally + { + ExitWriteLock(); + } + return true; + } + + public bool Remove(string key) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return false; + key = GetCacheKey(key); + try + { + + EnterWriteLock(); + items.Remove(key); + } + finally + { + ExitWriteLock(); + } + return true; + } + + #region Entries + + protected override IEnumerable GetDictionaryEntries() + { + const string prefix = CacheItemPrefix + "-"; + + if (!TryGetContextItems(out var items)) return Enumerable.Empty(); + + return items.Cast() + .Where(x => x.Key is string s && s.StartsWith(prefix)); + } + + protected override void RemoveEntry(string key) + { + if (!TryGetContextItems(out var items)) return; + + items.Remove(key); + } + + protected override object GetEntry(string key) + { + return !TryGetContextItems(out var items) ? null : items[key]; + } + + #endregion + + #region Lock + + private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered"; + + protected override void EnterReadLock() => EnterWriteLock(); + + protected override void EnterWriteLock() + { + if (!TryGetContextItems(out var items)) return; + + // note: cannot keep 'entered' as a class variable here, + // since there is one per request - so storing it within + // ContextItems - which is locked, so this should be safe + + var entered = false; + Monitor.Enter(items, ref entered); + items[ContextItemsLockKey] = entered; + } + + protected override void ExitReadLock() => ExitWriteLock(); + + protected override void ExitWriteLock() + { + if (!TryGetContextItems(out var items)) return; + + var entered = (bool?)items[ContextItemsLockKey] ?? false; + if (entered) + Monitor.Exit(items); + items.Remove(ContextItemsLockKey); + } + + #endregion + + public IEnumerator> GetEnumerator() + { + if (!TryGetContextItems(out var items)) + { + yield break; + } + + foreach (var item in items) + { + yield return new KeyValuePair(item.Key.ToString(), item.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs index 0bdfe5c425..b653f535fa 100644 --- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs +++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs @@ -6,6 +6,10 @@ namespace Umbraco.Core.Hosting { string SiteName { get; } string ApplicationId { get; } + + /// + /// Will return the physical path to the root of the application + /// 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 ? + + /// + /// Maps a virtual path to a physical path to the application's web root + /// + /// + /// + /// + /// 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. + /// string MapPath(string path); /// - /// 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 /// /// The virtual path. Must start with either ~/ or / else an exception is thrown. /// diff --git a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs new file mode 100644 index 0000000000..47e2d8fa7c --- /dev/null +++ b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Logging +{ + + public interface ILoggingConfiguration + { + /// + /// The physical path where logs are stored + /// + string LogDirectory { get; } + string LogConfigurationFile { get; } + string UserLogConfigurationFile { get; } + } +} diff --git a/src/Umbraco.Core/Logging/IProfiler.cs b/src/Umbraco.Core/Logging/IProfiler.cs index 1327651197..d855612c95 100644 --- a/src/Umbraco.Core/Logging/IProfiler.cs +++ b/src/Umbraco.Core/Logging/IProfiler.cs @@ -2,18 +2,12 @@ namespace Umbraco.Core.Logging { + /// /// Defines the profiling service. /// public interface IProfiler { - /// - /// Renders the profiling results. - /// - /// The profiling results. - /// Generally used for HTML rendering. - string Render(); - /// /// Gets an that will time the code between its creation and disposal. /// diff --git a/src/Umbraco.Core/Logging/IProfilerHtml.cs b/src/Umbraco.Core/Logging/IProfilerHtml.cs new file mode 100644 index 0000000000..4f9ee62e0b --- /dev/null +++ b/src/Umbraco.Core/Logging/IProfilerHtml.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Core.Logging +{ + /// + /// Used to render a profiler in a web page + /// + public interface IProfilerHtml + { + /// + /// Renders the profiling results. + /// + /// The profiling results. + /// Generally used for HTML rendering. + string Render(); + } +} diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs index 294f92dad3..a1d2a2e61f 100644 --- a/src/Umbraco.Core/Logging/LogProfiler.cs +++ b/src/Umbraco.Core/Logging/LogProfiler.cs @@ -15,12 +15,6 @@ namespace Umbraco.Core.Logging _logger = logger; } - /// - public string Render() - { - return string.Empty; - } - /// public IDisposable Step(string name) { diff --git a/src/Umbraco.Core/Logging/LoggingConfiguration.cs b/src/Umbraco.Core/Logging/LoggingConfiguration.cs new file mode 100644 index 0000000000..c657c9d430 --- /dev/null +++ b/src/Umbraco.Core/Logging/LoggingConfiguration.cs @@ -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; } + } +} diff --git a/src/Umbraco.Core/Logging/VoidProfiler.cs b/src/Umbraco.Core/Logging/VoidProfiler.cs index 51bec521a3..d771fd7630 100644 --- a/src/Umbraco.Core/Logging/VoidProfiler.cs +++ b/src/Umbraco.Core/Logging/VoidProfiler.cs @@ -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; diff --git a/src/Umbraco.Core/Net/NullSessionIdResolver.cs b/src/Umbraco.Core/Net/NullSessionIdResolver.cs new file mode 100644 index 0000000000..6bfa578268 --- /dev/null +++ b/src/Umbraco.Core/Net/NullSessionIdResolver.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Net +{ + public class NullSessionIdResolver : ISessionIdResolver + { + public string SessionId => null; + } +} diff --git a/src/Umbraco.Core/Web/IRequestAccessor.cs b/src/Umbraco.Core/Web/IRequestAccessor.cs index 8a92b6ee22..7dc77d3c29 100644 --- a/src/Umbraco.Core/Web/IRequestAccessor.cs +++ b/src/Umbraco.Core/Web/IRequestAccessor.cs @@ -9,5 +9,6 @@ namespace Umbraco.Web string GetQueryStringValue(string name); event EventHandler EndRequest; event EventHandler RouteAttempt; + Uri GetRequestUrl(); } } diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs index 2099778185..ab758d42af 100644 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -13,8 +13,17 @@ namespace Umbraco.Core.Composing /// /// public static IHostBuilder UseUmbraco(this IHostBuilder builder) - => builder.UseUmbraco(new UmbracoServiceProviderFactory()); + { + return builder + .UseUmbraco(new UmbracoServiceProviderFactory()); + } + /// + /// Assigns a custom service provider factory to use Umbraco's container + /// + /// + /// + /// public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory) => builder.UseServiceProviderFactory(umbracoServiceProviderFactory); } diff --git a/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs b/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs index 9bc0b1c3fb..57e9b5204b 100644 --- a/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs +++ b/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs @@ -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; diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs index 4640007e1a..712ff85e16 100644 --- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs +++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs @@ -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 MinimalLogger = new Lazy(() => 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); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs index 45468ace9f..704e80d302 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs @@ -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 /// - internal class HttpRequestIdEnricher : ILogEventEnricher + public class HttpRequestIdEnricher : ILogEventEnricher { - private readonly Func _requestCacheGetter; + private readonly IRequestCache _requestCache; - public HttpRequestIdEnricher(Func requestCacheGetter) + public HttpRequestIdEnricher(IRequestCache requestCache) { - _requestCacheGetter = requestCacheGetter; + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); } /// @@ -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)); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs index 08eb6b93f0..20643ff539 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs @@ -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 /// - internal class HttpRequestNumberEnricher : ILogEventEnricher + public class HttpRequestNumberEnricher : ILogEventEnricher { - private readonly Func _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 requestCacheGetter) + public HttpRequestNumberEnricher(IRequestCache requestCache) { - _requestCacheGetter = requestCacheGetter; + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); } /// @@ -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)); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs index 1558cdcf21..19572b5b42 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs @@ -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 /// - internal class HttpSessionIdEnricher : ILogEventEnricher + public class HttpSessionIdEnricher : ILogEventEnricher { private readonly ISessionIdResolver _sessionIdResolver; diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs new file mode 100644 index 0000000000..1f495d3a50 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs @@ -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 +{ + /// + /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can + /// + 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"); + } + + + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index f4e8f85281..dfcc401ea3 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -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 /// /// A Serilog LoggerConfiguration - /// - public static LoggerConfiguration MinimalConfiguration(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func requestCacheGetter) + /// + /// + 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() - .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/ /// /// A Serilog LoggerConfiguration + /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept - 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/ /// /// A Serilog LoggerConfiguration + /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept - 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 /// /// A Serilog LoggerConfiguration - public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig) + /// + 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 /// /// A Serilog LoggerConfiguration - public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig) + /// + 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; } + } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs new file mode 100644 index 0000000000..18b417d428 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs @@ -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(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index bb77869e28..38af9554ab 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -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 { + /// /// Implements on top of Serilog. /// public class SerilogLogger : ILogger, IDisposable { - private readonly ICoreDebugSettings _coreDebugSettings; - private readonly IIOHelper _ioHelper; - private readonly IMarchal _marchal; + public global::Serilog.ILogger SerilogLog { get; } /// /// Initialize a new instance of the class with a configuration file. /// /// - 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(); } /// /// Creates a logger with some pre-defined configuration and remainder from config file /// /// Used by UmbracoApplicationBase to get its logger. - public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func 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); } /// /// Gets a contextualized logger. /// private global::Serilog.ILogger LoggerFor(Type reporting) - => Log.Logger.ForContext(reporting); + => SerilogLog.ForContext(reporting); /// /// Maps Umbraco's log level to Serilog's. @@ -99,8 +83,7 @@ namespace Umbraco.Core.Logging.Serilog /// 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 /// 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); } /// 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); - } - /// public void Warn(Type reporting, string message) { @@ -289,7 +210,7 @@ namespace Umbraco.Core.Logging.Serilog public void Dispose() { - Log.CloseAndFlush(); + SerilogLog.DisposeIfDisposable(); } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index dbdd7842ba..6763b0ebbb 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -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 { diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs new file mode 100644 index 0000000000..14f35361e6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Logging.Viewer +{ + public interface ILogViewerConfig + { + IReadOnlyList GetSavedSearches(); + IReadOnlyList AddSavedSearch(string name, string query); + IReadOnlyList DeleteSavedSearch(string name, string query); + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs index 79680a3d53..ee115be325 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs @@ -9,7 +9,8 @@ namespace Umbraco.Core.Logging.Viewer { public void Compose(Composition composition) { - composition.SetLogViewer(factory => new JsonLogViewer(composition.Logger, factory.GetInstance())); + composition.RegisterUnique(); + composition.SetLogViewer(); } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs new file mode 100644 index 0000000000..5511cd87c7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -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 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(rawJson); + } + + public IReadOnlyList 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 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("[]"); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs similarity index 87% rename from src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs rename to src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index aea1c8fae4..366a0fb9de 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -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(); - //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(ex, "Unable to parse a line in the JSON log file"); + _logger.Error(ex, "Unable to parse a line in the JSON log file"); evt = null; return true; diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs similarity index 60% rename from src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs rename to src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index 4cc70eaf42..7c8503a37e 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -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 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(rawJson); - } + => _logViewerConfig.GetSavedSearches(); public virtual IReadOnlyList 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 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 /// public string GetLogLevel() { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.Logger.IsEnabled)?.Min() ?? null; + var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().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); - } - } + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index cfd67a5a02..dbf36da8f7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -64,6 +64,7 @@ namespace Umbraco.Core.Migrations.Upgrade { get { + // no state in database yet - assume we have something in web.config that makes some sense if (!SemVersion.TryParse(_globalSettings.ConfigurationStatus, out var currentVersion)) throw new InvalidOperationException($"Could not get current version from web.config {Constants.AppSettings.ConfigurationStatus} appSetting."); diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index c60aa57e16..38204d9452 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -30,6 +30,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; diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 92e47771a6..ebd91f52a2 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -25,6 +25,7 @@ namespace Umbraco.Core.Runtime private IFactory _factory; private readonly RuntimeState _state; private readonly IUmbracoBootPermissionChecker _umbracoBootPermissionChecker; + private readonly IRequestCache _requestCache; private readonly IGlobalSettings _globalSettings; private readonly IConnectionStrings _connectionStrings; @@ -39,7 +40,8 @@ namespace Umbraco.Core.Runtime IBackOfficeInfo backOfficeInfo, IDbProviderFactoryCreator dbProviderFactoryCreator, IMainDom mainDom, - ITypeFinder typeFinder) + ITypeFinder typeFinder, + IRequestCache requestCache) { IOHelper = ioHelper; Configs = configs; @@ -50,6 +52,7 @@ namespace Umbraco.Core.Runtime DbProviderFactoryCreator = dbProviderFactoryCreator; _umbracoBootPermissionChecker = umbracoBootPermissionChecker; + _requestCache = requestCache; Logger = logger; MainDom = mainDom; @@ -110,6 +113,7 @@ namespace Umbraco.Core.Runtime { if (register is null) throw new ArgumentNullException(nameof(register)); + // create and register the essential services // ie the bare minimum required to boot @@ -129,12 +133,19 @@ namespace Umbraco.Core.Runtime "Booted.", "Boot failed.")) { - Logger.Info("Booting Core"); + + Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.", + HostingEnvironment?.SiteName, + HostingEnvironment?.ApplicationId, + HostingEnvironment?.ApplicationPhysicalPath, + NetworkHelper.MachineName); Logger.Debug("Runtime: {Runtime}", GetType().FullName); // application environment ConfigureUnhandledException(); - return _factory = Configure(register, timer); + _factory = Configure(register, timer); + + return _factory; } } @@ -151,7 +162,7 @@ namespace Umbraco.Core.Runtime try { - + // run handlers RuntimeOptions.DoRuntimeBoot(ProfilingLogger); @@ -244,6 +255,13 @@ namespace Umbraco.Core.Runtime // create & initialize the components _components = _factory.GetInstance(); _components.Initialize(); + + + // now (and only now) is the time to switch over to perWebRequest scopes. + // up until that point we may not have a request, and scoped services would + // fail to resolve - but we run Initialize within a factory scope - and then, + // here, we switch the factory to bind scopes to requests + _factory.EnablePerWebRequestScope(); } protected virtual void ConfigureUnhandledException() @@ -350,7 +368,7 @@ namespace Umbraco.Core.Runtime return new AppCaches( new DeepCloneAppCache(new ObjectCacheAppCache()), - NoAppCache.Instance, + _requestCache, new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); } diff --git a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs b/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs deleted file mode 100644 index fc2a019023..0000000000 --- a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; - -namespace Umbraco.Web.Runtime -{ - /// - /// Represents the Web Umbraco runtime. - /// - /// On top of CoreRuntime, handles all of the web-related runtime aspects of Umbraco. - public class WebRuntime : CoreRuntime - { - private readonly IRequestCache _requestCache; - - /// - /// Initializes a new instance of the class. - /// - public WebRuntime( - Configs configs, - IUmbracoVersion umbracoVersion, - IIOHelper ioHelper, - ILogger logger, - IProfiler profiler, - IHostingEnvironment hostingEnvironment, - IBackOfficeInfo backOfficeInfo, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IMainDom mainDom, - ITypeFinder typeFinder, - IRequestCache requestCache, - IUmbracoBootPermissionChecker umbracoBootPermissionChecker): - base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder) - { - _requestCache = requestCache; - } - - /// - public override IFactory Configure(IRegister register) - { - - var profilingLogger = new ProfilingLogger(Logger, Profiler); - var umbracoVersion = new UmbracoVersion(); - using (var timer = profilingLogger.TraceDuration( - $"Booting Umbraco {umbracoVersion.SemanticVersion.ToSemanticString()}.", - "Booted.", - "Boot failed.")) - { - Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.", - HostingEnvironment.SiteName, - HostingEnvironment.ApplicationId, - HostingEnvironment.ApplicationPhysicalPath, - NetworkHelper.MachineName); - Logger.Debug("Runtime: {Runtime}", GetType().FullName); - - var factory = base.Configure(register); - - // now (and only now) is the time to switch over to perWebRequest scopes. - // up until that point we may not have a request, and scoped services would - // fail to resolve - but we run Initialize within a factory scope - and then, - // here, we switch the factory to bind scopes to requests - factory.EnablePerWebRequestScope(); - - return factory; - } - - - } - - #region Getters - - protected override AppCaches GetAppCaches() => new AppCaches( - // we need to have the dep clone runtime cache provider to ensure - // all entities are cached properly (cloned in and cloned out) - new DeepCloneAppCache(new ObjectCacheAppCache()), - // we need request based cache when running in web-based context - _requestCache, - new IsolatedCaches(type => - // we need to have the dep clone runtime cache provider to ensure - // all entities are cached properly (cloned in and cloned out) - new DeepCloneAppCache(new ObjectCacheAppCache()))); - - #endregion - } -} - diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs index 42b1e6c0dd..21b1f66395 100644 --- a/src/Umbraco.Tests.Common/TestHelperBase.cs +++ b/src/Umbraco.Tests.Common/TestHelperBase.cs @@ -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")); + } } } diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs index 6430291bc2..9f29b14858 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs @@ -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 diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 52c29d2037..ad040c8ef1 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -4,8 +4,10 @@ using Moq; using NUnit.Framework; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Smidge; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Runtime; @@ -57,7 +59,7 @@ namespace Umbraco.Tests.Integration var coreRuntime = new CoreRuntime(testHelper.GetConfigs(), testHelper.GetUmbracoVersion(), testHelper.IOHelper, testHelper.Logger, testHelper.Profiler, testHelper.UmbracoBootPermissionChecker, testHelper.GetHostingEnvironment(), testHelper.GetBackOfficeInfo(), testHelper.DbProviderFactoryCreator, - testHelper.MainDom, testHelper.GetTypeFinder()); + testHelper.MainDom, testHelper.GetTypeFinder(), NoAppCache.Instance); // boot it! var factory = coreRuntime.Configure(umbracoContainer); @@ -99,7 +101,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); @@ -138,7 +140,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 101feb79a4..0137850408 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -108,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests/Logging/LogviewerTests.cs b/src/Umbraco.Tests/Logging/LogviewerTests.cs index 87cc19a2c6..0a193b4446 100644 --- a/src/Umbraco.Tests/Logging/LogviewerTests.cs +++ b/src/Umbraco.Tests/Logging/LogviewerTests.cs @@ -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(); - _logViewer = new JsonLogViewer(logger, ioHelper, logsPath: _newLogfileDirPath, searchPath: _newSearchfilePath); + var logViewerConfig = new LogViewerConfig(hostingEnv); + _logViewer = new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, Log.Logger); } [OneTimeTearDown] diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index b900453a5e..960d355c0d 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -16,10 +16,10 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Core.Strings; using Umbraco.Core.Configuration; -using Umbraco.Core.Dictionary; using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Runtime; using Umbraco.Core.Services; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.Testing; @@ -47,10 +47,10 @@ namespace Umbraco.Tests.Routing HostingEnvironment); } - public class TestRuntime : WebRuntime + public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker()) + : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance) { } diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index 488a4f6dad..f63c56b64e 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -5,6 +5,7 @@ using Examine; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -16,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; @@ -116,7 +118,7 @@ namespace Umbraco.Tests.Runtimes public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder()) + :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance) { } @@ -134,6 +136,7 @@ namespace Umbraco.Tests.Runtimes public override IFactory Configure(IRegister container) { container.Register(Lifetime.Singleton); + container.Register(Lifetime.Singleton); var factory = base.Configure(container); return factory; diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 2a87fc7a43..67dc24e041 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -84,7 +84,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // determine actual runtime level runtimeState.DetermineRuntimeLevel(databaseFactory, logger); @@ -278,7 +278,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // get the components // all of them? diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs index 39cac6e24f..ea0f9cc44f 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs +++ b/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs @@ -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; diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 7eca49183d..6fcba1ba1a 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -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(); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 901192c609..80f6ab9c9e 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -264,7 +264,7 @@ namespace Umbraco.Tests.Testing profiler = Mock.Of(); 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: diff --git a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs index a6c544062e..0d55fd99d7 100644 --- a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs @@ -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)); } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs index ddf06e6532..a27113e881 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs @@ -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(); + 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(); + + return app; + } + public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); diff --git a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs index 0ed0fd658a..65c2be051f 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs @@ -2,9 +2,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core.Hosting; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Runtime; using Umbraco.Core.WebAssets; -using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.ActionResults; namespace Umbraco.Web.BackOffice.Filters @@ -14,10 +12,11 @@ namespace Umbraco.Web.BackOffice.Filters public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // logic before action goes here - var hostingEnvironment = context.HttpContext.RequestServices.GetService(); + var serviceProvider = context.HttpContext.RequestServices; + var hostingEnvironment = serviceProvider.GetService(); if (!hostingEnvironment.IsDebugMode) { - var runtimeMinifier = context.HttpContext.RequestServices.GetService(); + var runtimeMinifier = serviceProvider.GetService(); if (context.Result is JavaScriptResult jsResult) { diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index decfcfa660..b3ec11c241 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -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; diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs index 9ba0f06f64..f2a15e1fc7 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs @@ -1,6 +1,8 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Routing; namespace Umbraco.Web.Common.AspNetCore @@ -8,20 +10,39 @@ namespace Umbraco.Web.Common.AspNetCore public class AspNetCoreRequestAccessor : IRequestAccessor { private readonly IHttpContextAccessor _httpContextAccessor; - public AspNetCoreRequestAccessor(IHttpContextAccessor httpContextAccessor) + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public AspNetCoreRequestAccessor(IHttpContextAccessor httpContextAccessor, IUmbracoRequestLifetime umbracoRequestLifetime, IUmbracoContextAccessor umbracoContextAccessor) { _httpContextAccessor = httpContextAccessor; + _umbracoContextAccessor = umbracoContextAccessor; + + umbracoRequestLifetime.RequestStart += RequestStart; + umbracoRequestLifetime.RequestEnd += RequestEnd; } + private void RequestEnd(object sender, HttpContext e) + { + EndRequest?.Invoke(sender, new UmbracoRequestEventArgs(_umbracoContextAccessor.UmbracoContext)); + } + + private void RequestStart(object sender, HttpContext e) + { + var reason = EnsureRoutableOutcome.IsRoutable; + RouteAttempt?.Invoke(sender, new RoutableAttemptEventArgs(reason, _umbracoContextAccessor.UmbracoContext)); + } + + + public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name); public string GetFormValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Form[name]; public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name]; - //TODO implement public event EventHandler EndRequest; //TODO implement public event EventHandler RouteAttempt; + public Uri GetRequestUrl() => _httpContextAccessor.HttpContext != null ? new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()) : null; } } diff --git a/src/Umbraco.Web.Common/Constants/ViewConstants.cs b/src/Umbraco.Web.Common/Constants/ViewConstants.cs new file mode 100644 index 0000000000..5da1a74f55 --- /dev/null +++ b/src/Umbraco.Web.Common/Constants/ViewConstants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.Common.Constants +{ + /// + /// constants + /// + internal static class ViewConstants + { + internal const string ViewLocation = "~/Views"; + + internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; + } +} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs new file mode 100644 index 0000000000..43058616de --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Controllers +{ + public abstract class RenderController : Controller + { + + } +} diff --git a/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs new file mode 100644 index 0000000000..b33cbc7d8a --- /dev/null +++ b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Events +{ + public class ActionExecutedEventArgs : EventArgs + { + public Controller Controller { get; set; } + public object Model { get; set; } + + public ActionExecutedEventArgs(Controller controller, object model) + { + Controller = controller; + Model = model; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 843620d571..3facf1b77f 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Reflection; @@ -6,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; @@ -71,7 +77,22 @@ namespace Umbraco.Web.Common.Extensions var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer; - services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), out factory); + 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(httpContextAccessor); + + var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items); + + services.AddUmbracoCore(webHostEnvironment, + umbContainer, + Assembly.GetEntryAssembly(), + requestCache, + loggingConfig, + out factory); return services; } @@ -83,20 +104,34 @@ namespace Umbraco.Web.Common.Extensions /// /// /// + /// + /// + /// /// /// - public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly, out IFactory factory) + public static IServiceCollection AddUmbracoCore( + this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + IRegister umbContainer, + Assembly entryAssembly, + IRequestCache requestCache, + ILoggingConfiguration loggingConfiguration, + out IFactory factory) { if (services is null) throw new ArgumentNullException(nameof(services)); var container = umbContainer; if (container is null) throw new ArgumentNullException(nameof(container)); if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly)); - // Special case! The generic host adds a few default services but we need to manually add this one here NOW because - // we resolve it before the host finishes configuring in the call to CreateCompositionRoot - services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + var configs = serviceProvider.GetService(); - CreateCompositionRoot(services, webHostEnvironment, out var logger, out var configs, 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); @@ -109,14 +144,15 @@ namespace Umbraco.Web.Common.Extensions profiler, hostingEnvironment, backOfficeInfo, - CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly)); + CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly), + requestCache); factory = coreRuntime.Configure(container); 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. @@ -126,11 +162,12 @@ namespace Umbraco.Web.Common.Extensions return new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly), runtimeHash); } - private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, + private static IRuntime GetCoreRuntime( + Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, - ITypeFinder typeFinder) + ITypeFinder typeFinder, IRequestCache requestCache) { - var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName]; + var connectionStringConfig = configs.ConnectionStrings()[Core.Constants.System.UmbracoConnectionName]; var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator( connectionStringConfig?.ProviderName, DbProviderFactories.GetFactory); @@ -145,53 +182,92 @@ namespace Umbraco.Web.Common.Extensions var mainDom = new MainDom(logger, mainDomLock); - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(), - hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder); + var coreRuntime = new CoreRuntime( + configs, + umbracoVersion, + ioHelper, + logger, + profiler, + new AspNetCoreBootPermissionsChecker(), + hostingEnvironment, + backOfficeInfo, + dbProviderFactoryCreator, + mainDom, + typeFinder, + requestCache); return coreRuntime; } - private static IServiceCollection CreateCompositionRoot(IServiceCollection services, IWebHostEnvironment webHostEnvironment, - out ILogger logger, out Configs configs, 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) { - // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured - var serviceProvider = services.BuildServiceProvider(); - - var httpContextAccessor = serviceProvider.GetRequiredService(); - - configs = serviceProvider.GetService(); 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(), coreDebug, ioHelper, - new AspNetCoreMarchal()); + logger = AddLogger(services, hostingEnvironment, loggingConfiguration); backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings); - profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor); + profiler = GetWebProfiler(hostingEnvironment); return services; } + /// + /// Create and configure the logger + /// + /// + 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(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(diagnosticContext); + + return logger; + } + public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services, IConfiguration configuration) { - services.AddSmidge(configuration.GetSection(Constants.Configuration.ConfigRuntimeMinification)); + services.AddSmidge(configuration.GetSection(Core.Constants.Configuration.ConfigRuntimeMinification)); services.AddSmidgeNuglify(); 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) @@ -201,7 +277,7 @@ namespace Umbraco.Web.Common.Extensions return new VoidProfiler(); } - var webProfiler = new WebProfiler(httpContextAccessor); + var webProfiler = new WebProfiler(); webProfiler.StartBoot(); return webProfiler; @@ -216,4 +292,5 @@ namespace Umbraco.Web.Common.Extensions } + } diff --git a/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs new file mode 100644 index 0000000000..0fe251bac4 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Net.Http.Headers; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext context) + { + base.OnResultExecuting(context); + + var httpResponse = context.HttpContext.Response; + + if (httpResponse.StatusCode != 200) return; + + httpResponse.GetTypedHeaders().CacheControl = + new CacheControlHeaderValue() + { + NoCache = true, + MaxAge = TimeSpan.Zero, + MustRevalidate = true, + NoStore = true + }; + + httpResponse.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 + httpResponse.Headers[HeaderNames.Pragma] = "no-cache"; + httpResponse.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs new file mode 100644 index 0000000000..269e437d0d --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -0,0 +1,88 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Web.Common.Constants; +using Umbraco.Web.Common.Controllers; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// This is a special filter which is required for the RTE to be able to render Partial View Macros that + /// contain forms when the RTE value is resolved outside of an MVC view being rendered + /// + /// + /// The entire way that we support partial view macros that contain forms isn't really great, these forms + /// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action + /// so the form can show errors, viewdata, etc... + /// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases + /// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly + /// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is + /// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. + /// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that + /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains + /// a partial view macro form. + /// + public class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + { + + /// + /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, + /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still + /// render any PartialViewMacro with a form and maintain ModelState + /// + /// + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + /// + /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, + /// this ensures that any custom ModelState that may have been added in the RenderController itself is + /// passed onwards in case it is required when rendering a PartialViewMacro with a form + /// + /// The filter context. + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + private void SetViewContext(ActionContext context, Controller controller) + { + var viewCtx = new ViewContext( + context, + new DummyView(), + controller.ViewData, + controller.TempData, + new StringWriter(), + new HtmlHelperOptions()); + + //set the special data token + context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; + } + + private class DummyView : IView + { + public Task RenderAsync(ViewContext context) + { + return Task.CompletedTask; + } + + public string Path { get; } + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs new file mode 100644 index 0000000000..2ba58a8fd7 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Web.Common.Events; + +namespace Umbraco.Web.Common.Filters +{ + public class PreRenderViewActionFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext context) + { + if (!(context.Controller is Controller umbController) || !(context.Result is ViewResult result)) + { + return; + } + + var model = result.Model; + if (model == null) + { + return; + } + + var args = new ActionExecutedEventArgs(umbController, model); + OnActionExecuted(args); + + if (args.Model != model) + { + result.ViewData.Model = args.Model; + } + + base.OnActionExecuted(context); + } + + + public static event EventHandler ActionExecuted; + + private static void OnActionExecuted(ActionExecutedEventArgs e) + { + var handler = ActionExecuted; + handler?.Invoke(null, e); + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs new file mode 100644 index 0000000000..8f3fcf3a95 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs @@ -0,0 +1,39 @@ +using System.Net; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// Forces the response to have a specific http status code + /// + public class StatusCodeResultAttribute : ActionFilterAttribute + { + private readonly HttpStatusCode _statusCode; + + public StatusCodeResultAttribute(HttpStatusCode statusCode) + { + _statusCode = statusCode; + } + + public override void OnActionExecuted(ActionExecutedContext context) + { + base.OnActionExecuted(context); + + var httpContext = context.HttpContext; + + httpContext.Response.StatusCode = (int)_statusCode; + + var disableIisCustomErrors = httpContext.RequestServices.GetService().TrySkipIisCustomErrors; + var statusCodePagesFeature = httpContext.Features.Get(); + + if (statusCodePagesFeature != null) + { + // if IIS Custom Errors are disabled, we won't enable the Status Code Pages + statusCodePagesFeature.Enabled = !disableIisCustomErrors; + } + } + } +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs new file mode 100644 index 0000000000..0e2158c939 --- /dev/null +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs @@ -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); + } + } + } +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 93461fc1d5..e8695f3c9c 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -22,5 +22,4 @@ namespace Umbraco.Web.Common.Middleware _umbracoRequestLifetimeManager.EndRequest(context); } } - } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index f194826626..332a405580 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -11,6 +11,9 @@ using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.Macros; using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.Macros; +using Umbraco.Core.Diagnostics; +using Umbraco.Web.Common.Runtime.Profiler; +using Umbraco.Core.Logging; namespace Umbraco.Web.Common.Runtime { @@ -47,6 +50,10 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterMultipleUnique(); + composition.RegisterUnique(); + + composition.RegisterUnique(); + composition.RegisterUnique(); composition.RegisterUnique(); diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index bdbc6f164d..958e134bab 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -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; - } - - /// - /// - /// - /// - /// 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. - /// - 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{ 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); diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs index 688a3e5c28..523faf2da5 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs @@ -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, ICoreComposer { + public override void Compose(Composition composition) + { + base.Compose(composition); + + composition.RegisterUnique(); + } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs new file mode 100644 index 0000000000..9e989d6b5c --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs @@ -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; + } + + /// + /// + /// 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. + /// + 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 { 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; + } + } +} diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs index ba3c6c289f..c4b9522099 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Common.RuntimeMinification public void Reset() { var version = DateTime.UtcNow.Ticks.ToString(); - _configManipulator.SaveConfigValue(Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); + _configManipulator.SaveConfigValue(Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); } diff --git a/src/Umbraco.Web.Common/Security/WebSecurity.cs b/src/Umbraco.Web.Common/Security/WebSecurity.cs new file mode 100644 index 0000000000..7e0d5ac7f7 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/WebSecurity.cs @@ -0,0 +1,58 @@ +using System; +using System.Security; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Core.Models.Membership; +using Microsoft.AspNetCore.Http; +using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; +using Umbraco.Core.Models; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Security; + +namespace Umbraco.Web.Common.Security +{ + + /// + /// A utility class used for dealing with USER security in Umbraco + /// + public class WebSecurity : IWebSecurity + { + private IUser _currentUser; + + + public IUser CurrentUser + { + get => _currentUser; + set => _currentUser = value; + } + + public double PerformLogin(int userId) + { + return 15; + } + + public void ClearCurrentLogin() + { + + } + + public Attempt GetUserId() + { + return new Attempt(); + } + + public bool ValidateCurrentUser() + { + return false; + } + + public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true) => throw new NotImplementedException(); + + public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false) => throw new NotImplementedException(); + + public bool UserHasSectionAccess(string section, IUser user) => false; + + public bool IsAuthenticated() => false; + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index d62cd838a2..eed88bc491 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index dd7ce36cb5..9c86c85b5c 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -1,13 +1,9 @@ using System; -using System.Web; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -19,7 +15,6 @@ namespace Umbraco.Web /// public class UmbracoContext : DisposableObjectSlim, IDisposeOnRequestEnd, IUmbracoContext { - private readonly IHttpContextAccessor _httpContextAccessor; private readonly IGlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly ICookieManager _cookieManager; @@ -32,7 +27,7 @@ namespace Umbraco.Web // internal for unit tests // otherwise it's used by EnsureContext above // warn: does *not* manage setting any IUmbracoContextAccessor - internal UmbracoContext(IHttpContextAccessor httpContextAccessor, + internal UmbracoContext( IPublishedSnapshotService publishedSnapshotService, IWebSecurity webSecurity, IGlobalSettings globalSettings, @@ -42,26 +37,14 @@ namespace Umbraco.Web ICookieManager cookieManager, IRequestAccessor requestAccessor) { - if (httpContextAccessor == null) throw new ArgumentNullException(nameof(httpContextAccessor)); if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - _httpContextAccessor = httpContextAccessor; _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); _hostingEnvironment = hostingEnvironment; _cookieManager = cookieManager; _requestAccessor = requestAccessor; - // ensure that this instance is disposed when the request terminates, though we *also* ensure - // this happens in the Umbraco module since the UmbracoCOntext is added to the HttpContext items. - // - // also, it *can* be returned by the container with a PerRequest lifetime, meaning that the - // container *could* also try to dispose it. - // - // all in all, this context may be disposed more than once, but DisposableObject ensures that - // it is ok and it will be actually disposed only once. - httpContextAccessor.HttpContext?.DisposeOnPipelineCompleted(this); - ObjectCreated = DateTime.Now; UmbracoRequestId = Guid.NewGuid(); Security = webSecurity; @@ -76,7 +59,7 @@ namespace Umbraco.Web // the current domain during application startup. // see: http://issues.umbraco.org/issue/U4-1890 // - OriginalRequestUrl = new Uri(GetRequestFromContext()?.GetDisplayUrl() ?? "http://localhost"); + OriginalRequestUrl = _requestAccessor.GetRequestUrl() ?? new Uri("http://localhost"); CleanedUmbracoUrl = uriUtility.UriToUmbraco(OriginalRequestUrl); } @@ -183,9 +166,9 @@ namespace Umbraco.Web private void DetectPreviewMode() { - var request = GetRequestFromContext(); - if (request?.GetDisplayUrl() != null - && new Uri(request.GetEncodedUrl()).IsBackOfficeRequest(_globalSettings, _hostingEnvironment) == false + var requestUrl = _requestAccessor.GetRequestUrl(); + if (requestUrl != null + && requestUrl.IsBackOfficeRequest(_globalSettings, _hostingEnvironment) == false && Security.CurrentUser != null) { var previewToken = _cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); // may be null or empty @@ -204,18 +187,6 @@ namespace Umbraco.Web return PublishedSnapshot.ForcedPreview(preview, orig => InPreviewMode = orig); } - private HttpRequest GetRequestFromContext() - { - try - { - return _httpContextAccessor.HttpContext?.Request; - } - catch (Exception) - { - return null; - } - } - protected override void DisposeResources() { // DisposableObject ensures that this runs only once diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs index af1481e185..70be6161f3 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; +using Umbraco.Web.Common.Security; using Umbraco.Web.PublishedCache; using Umbraco.Web.Security; @@ -28,6 +29,7 @@ namespace Umbraco.Web private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ICookieManager _cookieManager; + private readonly IRequestAccessor _requestAccessor; private readonly UriUtility _uriUtility; /// @@ -43,7 +45,8 @@ namespace Umbraco.Web IHostingEnvironment hostingEnvironment, UriUtility uriUtility, IHttpContextAccessor httpContextAccessor, - ICookieManager cookieManager) + ICookieManager cookieManager, + IRequestAccessor requestAccessor) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); @@ -55,6 +58,7 @@ namespace Umbraco.Web _uriUtility = uriUtility; _httpContextAccessor = httpContextAccessor; _cookieManager = cookieManager; + _requestAccessor = requestAccessor; } private IUmbracoContext CreateUmbracoContext() @@ -70,9 +74,17 @@ namespace Umbraco.Web _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); } - var webSecurity = new WebSecurity(_httpContextAccessor, _userService, _globalSettings, _hostingEnvironment); + var webSecurity = new WebSecurity(); - return new UmbracoContext(_httpContextAccessor, _publishedSnapshotService, webSecurity, _globalSettings, _hostingEnvironment, _variationContextAccessor, _uriUtility, _cookieManager); + return new UmbracoContext( + _publishedSnapshotService, + webSecurity, + _globalSettings, + _hostingEnvironment, + _variationContextAccessor, + _uriUtility, + _cookieManager, + _requestAccessor); } /// diff --git a/src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js b/src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js new file mode 100644 index 0000000000..345fe23764 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js @@ -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%'" + } +] diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config new file mode 100644 index 0000000000..9aca408b36 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.config new file mode 100644 index 0000000000..9aca408b36 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config new file mode 100644 index 0000000000..8f207406e3 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.user.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.config new file mode 100644 index 0000000000..8f207406e3 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.config @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index f0504d77e3..1151f16be8 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -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 diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 468151fc9d..8305ed5a30 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -72,6 +72,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 (), factory.GetInstance(), @@ -93,7 +94,9 @@ namespace Umbraco.Web.UI.BackOffice { app.UseDeveloperExceptionPage(); } + app.UseStatusCodePages(); app.UseUmbracoCore(); + app.UseUmbracoRequestLogging(); app.UseUmbracoWebsite(); app.UseUmbracoBackOffice(); app.UseRouting(); @@ -117,7 +120,8 @@ namespace Umbraco.Web.UI.BackOffice endpoints.MapGet("/", async context => { - await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); + var profilerHtml = app.ApplicationServices.GetRequiredService(); + await context.Response.WriteAsync($"Hello World!{profilerHtml.Render()}"); }); }); } diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 4b925e81c4..8e1e656a6f 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -50,4 +50,37 @@ + + + + + + + + + + + + + + + + Designer + serilog.config + + + Designer + serilog.user.config + + + + + + Designer + + + Designer + + + diff --git a/src/Umbraco.Web.UI/config/serilog.Release.config b/src/Umbraco.Web.UI/config/serilog.Release.config index e3cf52b3c5..9aca408b36 100644 --- a/src/Umbraco.Web.UI/config/serilog.Release.config +++ b/src/Umbraco.Web.UI/config/serilog.Release.config @@ -19,7 +19,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs b/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs index a82c5cab63..66a21af078 100644 --- a/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs +++ b/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs @@ -38,5 +38,6 @@ namespace Umbraco.Web.AspNet } public event EventHandler EndRequest; public event EventHandler RouteAttempt; + public Uri GetRequestUrl() => _httpContextAccessor.HttpContext?.Request.Url; } } diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index f9f056ff32..1ed217cc78 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -248,6 +248,8 @@ namespace Umbraco.Web.Composing public static IProfiler Profiler => Factory.GetInstance(); + public static IProfilerHtml ProfilerHtml => Factory.GetInstance(); + public static IProfilingLogger ProfilingLogger => Factory.GetInstance(); public static AppCaches AppCaches => Factory.GetInstance(); diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 6c207dd15a..2795fe66c6 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web /// public static IHtmlString RenderProfiler(this HtmlHelper helper) { - return new HtmlString(Current.Profiler.Render()); + return new HtmlString(Current.ProfilerHtml.Render()); } /// diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index e390950c0b..6f15be7ecd 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Logging /// /// Profiling only runs when the app is in debug mode, see WebRuntime for how this gets created /// - internal class WebProfiler : IProfiler + internal class WebProfiler : IProfiler, IProfilerHtml { private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; private readonly WebProfilerProvider _provider; diff --git a/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs b/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs index 1c596ff80c..6904aa103a 100644 --- a/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs +++ b/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs @@ -3,6 +3,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { + /// Migrated already to .Net Core public class ActionExecutedEventArgs : EventArgs { public Controller Controller { get; set; } diff --git a/src/Umbraco.Web/Mvc/Constants.cs b/src/Umbraco.Web/Mvc/Constants.cs index 1794345746..c71ed6b104 100644 --- a/src/Umbraco.Web/Mvc/Constants.cs +++ b/src/Umbraco.Web/Mvc/Constants.cs @@ -3,6 +3,7 @@ /// /// constants /// + /// Migrated already to .Net Core internal static class Constants { internal const string ViewLocation = "~/Views"; diff --git a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs index 34b857dfb4..f443abbb70 100644 --- a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.Mvc /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains /// a partial view macro form. /// + /// Migrated already to .Net Core internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute { /// diff --git a/src/Umbraco.Web/Mvc/IRenderController.cs b/src/Umbraco.Web/Mvc/IRenderController.cs index 103a484869..0de585959c 100644 --- a/src/Umbraco.Web/Mvc/IRenderController.cs +++ b/src/Umbraco.Web/Mvc/IRenderController.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.Mvc /// /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking /// + /// Migrated already to .Net Core public interface IRenderController : IController { diff --git a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs index 227c15b344..635a7314c5 100644 --- a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs @@ -12,6 +12,7 @@ namespace Umbraco.Web.Mvc /// /// Only minifies in release mode /// + /// Migrated already to .Net Core public class MinifyJavaScriptResultAttribute : ActionFilterAttribute { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs index 54e20f5d56..2e659eccf6 100644 --- a/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs @@ -3,6 +3,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { + /// Migrated already to .Net Core public class PreRenderViewActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) diff --git a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs index b1836c6ba4..727c29b93c 100644 --- a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Mvc /// /// Forces the response to have a specific http status code /// + /// Migrated already to .Net Core internal class StatusCodeResultAttribute : ActionFilterAttribute { private readonly HttpStatusCode _statusCode; diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 0b70121d38..36a997c2b8 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -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.RegisterUnique(); + composition.RegisterUnique(); composition.ComposeWebMappingProfiles(); diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index cd72b2faf9..7679da2e2e 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,4 +1,7 @@ -using System.Threading; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Cache; @@ -34,10 +37,12 @@ namespace Umbraco.Web var mainDom = new MainDom(logger, mainDomLock); - var requestCache = new HttpRequestAppCache(() => HttpContext.Current?.Items); + var requestCache = new HttpRequestAppCache(() => HttpContext.Current != null ? HttpContext.Current.Items : null); var umbracoBootPermissionChecker = new AspNetUmbracoBootPermissionChecker(); - return new WebRuntime(configs, umbracoVersion, ioHelper, logger, profiler, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, - GetTypeFinder(hostingEnvironment, logger, profiler), requestCache, umbracoBootPermissionChecker); + return new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, + GetTypeFinder(hostingEnvironment, logger, profiler), requestCache); } + + } } diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index d884366bf1..1f90bc7d13 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -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(), 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())); + LogContext.Push(new HttpRequestNumberEnricher(_factory.GetInstance())); + LogContext.Push(new HttpRequestIdEnricher(_factory.GetInstance())); + _runtime.Start(); }