using log4net; using log4net.Config; using log4net.Repository; using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Extensions; using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Scope; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; using log4net.Appender; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Logging { /// /// The log4net provider class. /// /// public class Log4NetProvider : ILoggerProvider, ISupportExternalScope { /// /// The loggers collection. /// private readonly ConcurrentDictionary loggers = new ConcurrentDictionary(); /// /// Prevents to dispose the object more than single time. /// private bool disposedValue = false; /// /// The log4net repository. /// public ILoggerRepository Repository; /// /// The provider options. /// private Log4NetProviderOptions options; /// /// The external logging scope provider. /// /// /// Reading the offical logging implementations, it seems like we need to handle the case that this might never be set. /// We handle it with a NullScopeProvider instead of null checks, to make the process of implementing interfaces like /// less error prone for consumers. /// public IExternalScopeProvider ExternalScopeProvider { get; private set; } = NullScopeProvider.Instance; /// /// Initializes a new instance of the class. /// public Log4NetProvider() : this(new Log4NetProviderOptions()) { } /// /// Initializes a new instance of the class. /// /// The log4NetConfigFile. public Log4NetProvider(string log4NetConfigFileName) : this(new Log4NetProviderOptions(log4NetConfigFileName)) { } /// /// Initializes a new instance of the class. /// /// The options. /// options /// Wach cannot be true when you are overwriting config file values with values from configuration section. public Log4NetProvider(Log4NetProviderOptions options) { this.SetOptionsIfValid(options); Assembly loggingAssembly = GetLoggingReferenceAssembly(); this.CreateLoggerRepository(loggingAssembly) .ConfigureLog4NetLibrary(loggingAssembly); } #region IOC implementation /// /// Initializes a new instance of the class. /// /// A reference to the IOC service collection. public Log4NetProvider(IServiceProvider serviceCollection) : this(new Log4NetProviderOptions(), serviceCollection) { } /// /// Initializes a new instance of the class. /// /// The log4NetConfigFile. /// A reference to the IOC service collection. public Log4NetProvider(string log4NetConfigFileName, IServiceProvider serviceProvider) : this(new Log4NetProviderOptions(log4NetConfigFileName), serviceProvider) { } /// /// Initializes a new instance of the class. /// /// The options. /// A reference to the IOC service collection. /// options /// Watch cannot be true when you are overwriting config file values with values from configuration section. public Log4NetProvider(Log4NetProviderOptions options, IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; this.SetOptionsIfValid(options); Assembly loggingAssembly = GetLoggingReferenceAssembly(); this.CreateLoggerRepository(loggingAssembly) .ConfigureLog4NetLibrary(loggingAssembly); } /// /// Holds a reference to the IOC Service Provider /// private IServiceProvider serviceProvider; #endregion /// /// Finalizes the instance of the object. /// ~Log4NetProvider() { Dispose(false); } /// /// Creates the logger. /// /// An instance of the . public ILogger CreateLogger() => this.CreateLogger(this.options.Name); /// /// Creates the logger. /// /// The category name. /// An instance of the . public ILogger CreateLogger(string categoryName) => this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation); /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { this.Repository.Shutdown(); this.loggers.Clear(); serviceProvider = null; } disposedValue = true; } } /// /// Updates configuration nodes overriding values if required. /// /// The configuration file XML document. /// The overriding values available /// An within the overriding values replaced. private static XmlDocument UpdateNodesWithOverridingValues(XmlDocument configXmlDocument, IEnumerable overridingNodes) { var additionalConfig = overridingNodes; if (additionalConfig != null) { var configXDoc = configXmlDocument.ToXDocument(); foreach (var nodeInfo in additionalConfig) { var node = configXDoc.XPathSelectElement(nodeInfo.XPath); if (node != null) { if (nodeInfo.NodeContent != null) { node.Value = nodeInfo.NodeContent; } AddOrUpdateAttributes(node, nodeInfo); } } return configXDoc.ToXmlDocument(); } return configXmlDocument; } /// /// Adds or updates the attributes specified in the node information. /// /// The node. /// The node information. private static void AddOrUpdateAttributes(XElement node, NodeInfo nodeInfo) { if (nodeInfo?.Attributes != null) { foreach (var attribute in nodeInfo.Attributes) { var nodeAttribute = node.Attributes() .FirstOrDefault(a => a.Name.LocalName.Equals(attribute.Key, StringComparison.OrdinalIgnoreCase)); if (nodeAttribute != null) { nodeAttribute.Value = attribute.Value; } else { node.SetAttributeValue(attribute.Key, attribute.Value); } } } } /// /// Parses log4net config file. /// /// The filename. /// The with the log4net XML element. private static XmlDocument ParseLog4NetConfigFile(string filename) { using (FileStream stream = File.OpenRead(filename)) { var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit }; var log4netConfig = new XmlDocument(); using (var reader = XmlReader.Create(stream, settings)) { log4netConfig.Load(reader); } return log4netConfig; } } /// /// Tries to retrieve the assembly from a "Startup" type found in the stack trace. /// /// Null for NetCoreApp 1.1, otherwise, Assembly of Startup type if found in stack trace. private static Assembly GetCallingAssemblyFromStartup() { var stackTrace = new System.Diagnostics.StackTrace(2); for (int i = 0; i < stackTrace.FrameCount; i++) { var frame = stackTrace.GetFrame(i); var type = frame.GetMethod()?.DeclaringType; if (string.Equals(type?.Name, "Startup", StringComparison.OrdinalIgnoreCase)) { return type.Assembly; } } return null; } /// /// Creates the logger implementation. /// /// The name. /// The instance. private Log4NetLogger CreateLoggerImplementation(string name) { var loggerOptions = new Log4NetProviderOptions { Name = name, LoggerRepository = this.Repository.Name, OverrideCriticalLevelWith = this.options.OverrideCriticalLevelWith, LoggingEventFactory = this.options.LoggingEventFactory ?? new Log4NetLoggingEventFactory(), LogLevelTranslator = this.options.LogLevelTranslator ?? new Log4NetLogLevelTranslator(), }; return new Log4NetLogger(loggerOptions, ExternalScopeProvider); } /// /// Gets the current executing assembly considering the target framework. /// /// The assembly to be used as the reference logging assembly. private static Assembly GetLoggingReferenceAssembly() { Assembly assembly = null; assembly = Assembly.GetExecutingAssembly(); return assembly ?? GetCallingAssemblyFromStartup(); } /// /// Ensures that provided options combinations are valid, and sets the class field if everything is ok. /// /// The options to validate. /// /// Throws when the Watch option is set and there are properties to override. /// /// /// Throws when the options parameter is null. /// private void SetOptionsIfValid(Log4NetProviderOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } if (options.Watch && options.PropertyOverrides.Any()) { throw new NotSupportedException("Wach cannot be true when you are overwriting config file values with values from configuration section."); } this.options = options; } /// /// Configures the log4net library using the available configuration data. /// /// The assembly to be used on the configuration. private Log4NetProvider ConfigureLog4NetLibrary(Assembly assembly) { if (this.options.UseWebOrAppConfig) { XmlConfigurator.Configure(this.Repository); return this; } if (!this.options.ExternalConfigurationSetup) { string fileNamePath = CreateLog4NetFilePath(assembly); if (this.options.Watch) { XmlConfigurator.ConfigureAndWatch( this.Repository, new FileInfo(fileNamePath)); } else { var configXml = ParseLog4NetConfigFile(fileNamePath); if (this.options.PropertyOverrides != null && this.options.PropertyOverrides.Any()) { configXml = UpdateNodesWithOverridingValues( configXml, this.options.PropertyOverrides); } XmlConfigurator.Configure(this.Repository, configXml.DocumentElement); } } this.InjectServices(); return this; } /// /// Wires up specific adapters for IOC support /// private void InjectServices() { if (this.Repository is null) return; IEnumerable adapters = this.Repository .GetAppenders() .OfType(); foreach (IAppenderServiceProvider adapter in adapters) adapter.ServiceProvider = serviceProvider; } /// /// Creates the log4net.config file path. /// /// The assembly to be used when the configuration indicate to use the current assembly. /// The full path to the log4net.config file. private string CreateLog4NetFilePath(Assembly assembly) { string fileNamePath = this.options.Log4NetConfigFileName; if (!Path.IsPathRooted(fileNamePath)) { fileNamePath = Path.Combine(AppContext.BaseDirectory, fileNamePath); } return Path.GetFullPath(fileNamePath); } /// /// Gets or creates the logger repository using the given assembly. /// /// The assembly to be used to create de repository. private Log4NetProvider CreateLoggerRepository(Assembly assembly) { Type repositoryType = typeof(log4net.Repository.Hierarchy.Hierarchy); if (!string.IsNullOrEmpty(this.options.LoggerRepository)) { try { this.Repository = LogManager.GetRepository(this.options.LoggerRepository); if (this.options.ExternalConfigurationSetup) { // The logger repository is already configured. We can exit here. return this; } } catch (log4net.Core.LogException) { // The logger repository is not defined outside the extension. this.Repository = null; } if (this.Repository == null) { this.Repository = LogManager.CreateRepository(this.options.LoggerRepository, repositoryType); } } else { this.Repository = LogManager.CreateRepository(assembly, repositoryType); } return this; } public void SetScopeProvider(IExternalScopeProvider scopeProvider) { ExternalScopeProvider = scopeProvider; } } }