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;
}
}
}