Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

using System.Reflection;
using System.Runtime.Serialization;
using AopAlliance.Intercept;
using Microsoft.Extensions.Logging;
Expand All @@ -33,7 +32,9 @@ namespace Spring.Aspects.Logging;
public abstract class AbstractLoggingAdvice : IMethodInterceptor, IDeserializationCallback
{
/// <summary>
/// The default <code>ILog</code> instance used to write logging messages.
/// The explicitly supplied <see cref="ILogger"/> instance used to write logging messages,
/// if any. When <c>null</c>, the logger is resolved through <see cref="LogManager"/>,
/// either by <see cref="defaultLoggerName"/> or dynamically per invocation.
/// </summary>
[NonSerialized] protected ILogger defaultLogger;

Expand All @@ -52,7 +53,7 @@ public abstract class AbstractLoggingAdvice : IMethodInterceptor, IDeserializati
/// </summary>
protected AbstractLoggingAdvice()
{
SetDefaultLogger(MethodBase.GetCurrentMethod().DeclaringType.FullName);
SetDefaultLogger(GetType().FullName);
}

/// <summary>
Expand Down Expand Up @@ -89,8 +90,9 @@ public bool UseDynamicLogger
/// Sets the name of the logger to use.
/// </summary>
/// <remarks>
/// The name will be passed to the underlying logging implementation through Common.Logging,
/// getting interpreted as the log category according to the loggers configuration.
/// The name will be passed to the underlying logging implementation through the configured
/// <see cref="LogManager.LoggerFactory"/>, getting interpreted as the log category
/// according to the loggers configuration.
/// <para>
/// This can be specified to not log into the category of a Type (whether this
/// interceptor's class or the class getting called) but rather to a specific named category.
Expand Down Expand Up @@ -227,26 +229,35 @@ protected virtual ILogger GetLoggerForInvocation(IMethodInvocation invocation)
{
return defaultLogger;
}
else

if (defaultLoggerName != null)
{
object target = invocation.This;
Type logCategoryType = target.GetType();
if (hideProxyTypeNames)
{
logCategoryType = AopUtils.GetTargetType(target);
}
return LogManager.GetLogger(defaultLoggerName);
}

return LogManager.GetLogger(logCategoryType);
object target = invocation.This;
Type logCategoryType = target.GetType();
if (hideProxyTypeNames)
{
logCategoryType = AopUtils.GetTargetType(target);
}

return LogManager.GetLogger(logCategoryType);
}

/// <summary>
/// Sets the default logger to the given name.
/// Sets the name of the default logger.
/// </summary>
/// <param name="name">if <c>null</c>, the default logger is removed.</param>
/// <remarks>
/// The logger is resolved lazily through <see cref="LogManager"/> on each invocation,
/// so a <see cref="LogManager.LoggerFactory"/> assigned after this advice has been
/// created is still picked up.
/// </remarks>
/// <param name="name">if <c>null</c>, the default logger is removed and a dynamic,
/// per-target logger is used instead.</param>
protected void SetDefaultLogger(string name)
{
defaultLogger = (name == null ? null : LogManager.GetLogger(name));
defaultLogger = null;
defaultLoggerName = name;
}

Expand Down
6 changes: 6 additions & 0 deletions src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ public string Separator
/// <summary>
/// Gets or sets the entry log level.
/// </summary>
/// <remarks>
/// When configured from a string value, the legacy Common.Logging level names used by
/// Spring.NET configurations prior to 3.0 are also accepted: <c>All</c> maps to
/// <c>Trace</c>, <c>Info</c> to <c>Information</c>, <c>Warn</c> to <c>Warning</c>,
/// <c>Fatal</c> to <c>Critical</c> and <c>Off</c> to <c>None</c>.
/// </remarks>
/// <value>The entry log level.</value>
public LogLevel LogLevel
{
Expand Down
80 changes: 80 additions & 0 deletions src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2002-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.ComponentModel;
using System.Globalization;
using Microsoft.Extensions.Logging;

namespace Spring.Core.TypeConversion;

/// <summary>
/// Converter for <see cref="Microsoft.Extensions.Logging.LogLevel"/> instances.
/// </summary>
/// <remarks>
/// In addition to the <see cref="Microsoft.Extensions.Logging.LogLevel"/> member names
/// (<c>Trace</c>, <c>Debug</c>, <c>Information</c>, <c>Warning</c>, <c>Error</c>,
/// <c>Critical</c>, <c>None</c>), the legacy Common.Logging level names used by
/// Spring.NET configurations prior to 3.0 are accepted: <c>All</c> maps to
/// <c>Trace</c>, <c>Info</c> to <c>Information</c>, <c>Warn</c> to <c>Warning</c>,
/// <c>Fatal</c> to <c>Critical</c> and <c>Off</c> to <c>None</c>.
/// Names are matched case-insensitively.
/// </remarks>
public class LogLevelConverter : EnumConverter
{
private static readonly Dictionary<string, LogLevel> LegacyLevels = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase)
{
["All"] = LogLevel.Trace,
["Info"] = LogLevel.Information,
["Warn"] = LogLevel.Warning,
["Fatal"] = LogLevel.Critical,
["Off"] = LogLevel.None
};

/// <summary>
/// Creates a new instance of the
/// <see cref="Spring.Core.TypeConversion.LogLevelConverter"/> class.
/// </summary>
public LogLevelConverter() : base(typeof(LogLevel))
{
}

/// <summary>
/// Convert from a string value to a <see cref="Microsoft.Extensions.Logging.LogLevel"/> instance.
/// </summary>
/// <param name="context">
/// A <see cref="System.ComponentModel.ITypeDescriptorContext"/>
/// that provides a format context.
/// </param>
/// <param name="culture">
/// The <see cref="System.Globalization.CultureInfo"/> to use
/// as the current culture.
/// </param>
/// <param name="value">
/// The value that is to be converted.
/// </param>
/// <returns>
/// A <see cref="Microsoft.Extensions.Logging.LogLevel"/> if successful.
/// </returns>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string text && LegacyLevels.TryGetValue(text.Trim(), out LogLevel level))
{
return level;
}

return base.ConvertFrom(context, culture, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.Net;
using System.Resources;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using Spring.Core.TypeResolution;
using Spring.Util;
Expand Down Expand Up @@ -57,6 +58,7 @@ static TypeConverterRegistry()
converters[typeof(ResourceManager)] = new ResourceManagerConverter();
converters[typeof(Regex)] = new RegexConverter();
converters[typeof(TimeSpan)] = new TimeSpanConverter();
converters[typeof(LogLevel)] = new LogLevelConverter();
converters[typeof(ICredentials)] = new CredentialConverter();
converters[typeof(NetworkCredential)] = new CredentialConverter();
converters[typeof(RegistryKey)] = new RegistryKeyConverter();
Expand Down
6 changes: 6 additions & 0 deletions src/Spring/Spring.Core/LogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public static class LogManager
/// <summary>
/// Gets or sets the current log provider based on logger factory.
/// </summary>
/// <remarks>
/// Until a factory is assigned, all loggers returned by the <c>GetLogger</c> methods
/// are no-op <see cref="NullLogger"/> instances. Assigning a factory takes effect for
/// all loggers resolved afterwards, including lazily resolved ones such as those used
/// by <c>Spring.Aspects.Logging.AbstractLoggingAdvice</c>.
/// </remarks>
public static ILoggerFactory LoggerFactory { get; set; }

public static ILogger GetLogger(string category) => LoggerFactory?.CreateLogger(category) ?? NullLogger.Instance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
*/

using System.Reflection;
using System.Text;
using AopAlliance.Intercept;
using FakeItEasy;
using FakeItEasy.Configuration;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Spring.Aop.Framework;
using Spring.Core.IO;
using Spring.Objects.Factory.Xml;

namespace Spring.Aspects.Logging;

Expand All @@ -43,9 +46,18 @@ public void DoSomething()
}
}

private ILoggerFactory originalLoggerFactory;

[SetUp]
public void Setup()
{
originalLoggerFactory = LogManager.LoggerFactory;
}

[TearDown]
public void TearDown()
{
LogManager.LoggerFactory = originalLoggerFactory;
}

[Test]
Expand Down Expand Up @@ -164,6 +176,93 @@ public void SunnyDayLoggingAllOptionalInformationCorrectly()
log.VerifyLogMustHaveHappened(LogLevel.Trace, "Entering Bark");
log.VerifyLogMustHaveHappened(LogLevel.Trace, "Exiting Bark");
}

[Test]
public void LoggerFactoryAssignedAfterAdviceConstructionIsUsed()
{
LogManager.LoggerFactory = null;
SimpleLoggingAdvice loggingAdvice = new SimpleLoggingAdvice();

ProxyFactory pf = new ProxyFactory(new TestTarget());
pf.AddAdvice(loggingAdvice);
ITestTarget ptt = (ITestTarget) pf.GetProxy();

RecordingLoggerFactory loggerFactory = new RecordingLoggerFactory();
LogManager.LoggerFactory = loggerFactory;

ptt.DoSomething();

Assert.That(loggerFactory.Messages, Has.Some.Contains("Entering DoSomething"));
Assert.That(loggerFactory.Messages, Has.Some.Contains("Exiting DoSomething"));
}

[Test]
public void DefaultLoggerCategoryIsConcreteAdviceType()
{
RecordingLoggerFactory loggerFactory = new RecordingLoggerFactory();
LogManager.LoggerFactory = loggerFactory;

SimpleLoggingAdvice loggingAdvice = new SimpleLoggingAdvice();
ProxyFactory pf = new ProxyFactory(new TestTarget());
pf.AddAdvice(loggingAdvice);
((ITestTarget) pf.GetProxy()).DoSomething();

Assert.That(loggerFactory.Categories, Does.Contain(typeof(SimpleLoggingAdvice).FullName));
}

[Test]
public void LegacyLogLevelNameCanBeConfiguredFromXml()
{
string xml = $@"<?xml version='1.0' encoding='UTF-8' ?>
<objects xmlns='http://www.springframework.net'>
<object id='loggingAdvice' type='{typeof(SimpleLoggingAdvice).AssemblyQualifiedName}'>
<property name='LogLevel' value='Info'/>
</object>
</objects>";

XmlObjectFactory objectFactory = new XmlObjectFactory(new StringResource(xml, Encoding.UTF8));
SimpleLoggingAdvice advice = (SimpleLoggingAdvice) objectFactory.GetObject("loggingAdvice");

Assert.That(advice.LogLevel, Is.EqualTo(LogLevel.Information));
}

private sealed class RecordingLoggerFactory : ILoggerFactory
{
public List<string> Categories { get; } = [];
public List<string> Messages { get; } = [];

public ILogger CreateLogger(string categoryName)
{
Categories.Add(categoryName);
return new RecordingLogger(Messages);
}

public void AddProvider(ILoggerProvider provider)
{
}

public void Dispose()
{
}

private sealed class RecordingLogger(List<string> messages) : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
messages.Add(formatter(state, exception));
}

public bool IsEnabled(LogLevel logLevel)
{
return true;
}

public IDisposable BeginScope<TState>(TState state)
{
return null;
}
}
}
}

public class Dog
Expand Down
Loading