diff --git a/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs b/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs index 3c14126b7..176872698 100644 --- a/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs +++ b/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs @@ -14,7 +14,6 @@ * limitations under the License. */ -using System.Reflection; using System.Runtime.Serialization; using AopAlliance.Intercept; using Microsoft.Extensions.Logging; @@ -33,7 +32,9 @@ namespace Spring.Aspects.Logging; public abstract class AbstractLoggingAdvice : IMethodInterceptor, IDeserializationCallback { /// - /// The default ILog instance used to write logging messages. + /// The explicitly supplied instance used to write logging messages, + /// if any. When null, the logger is resolved through , + /// either by or dynamically per invocation. /// [NonSerialized] protected ILogger defaultLogger; @@ -52,7 +53,7 @@ public abstract class AbstractLoggingAdvice : IMethodInterceptor, IDeserializati /// protected AbstractLoggingAdvice() { - SetDefaultLogger(MethodBase.GetCurrentMethod().DeclaringType.FullName); + SetDefaultLogger(GetType().FullName); } /// @@ -89,8 +90,9 @@ public bool UseDynamicLogger /// Sets the name of the logger to use. /// /// - /// 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 + /// , getting interpreted as the log category + /// according to the loggers configuration. /// /// 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. @@ -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); } /// - /// Sets the default logger to the given name. + /// Sets the name of the default logger. /// - /// if null, the default logger is removed. + /// + /// The logger is resolved lazily through on each invocation, + /// so a assigned after this advice has been + /// created is still picked up. + /// + /// if null, the default logger is removed and a dynamic, + /// per-target logger is used instead. protected void SetDefaultLogger(string name) { - defaultLogger = (name == null ? null : LogManager.GetLogger(name)); + defaultLogger = null; defaultLoggerName = name; } diff --git a/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs b/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs index 25e2df22c..d8173a4db 100644 --- a/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs +++ b/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs @@ -140,6 +140,12 @@ public string Separator /// /// Gets or sets the entry log level. /// + /// + /// When configured from a string value, the legacy Common.Logging level names used by + /// Spring.NET configurations prior to 3.0 are also accepted: All maps to + /// Trace, Info to Information, Warn to Warning, + /// Fatal to Critical and Off to None. + /// /// The entry log level. public LogLevel LogLevel { diff --git a/src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs b/src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs new file mode 100644 index 000000000..b4529f3f3 --- /dev/null +++ b/src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs @@ -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; + +/// +/// Converter for instances. +/// +/// +/// In addition to the member names +/// (Trace, Debug, Information, Warning, Error, +/// Critical, None), the legacy Common.Logging level names used by +/// Spring.NET configurations prior to 3.0 are accepted: All maps to +/// Trace, Info to Information, Warn to Warning, +/// Fatal to Critical and Off to None. +/// Names are matched case-insensitively. +/// +public class LogLevelConverter : EnumConverter +{ + private static readonly Dictionary LegacyLevels = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["All"] = LogLevel.Trace, + ["Info"] = LogLevel.Information, + ["Warn"] = LogLevel.Warning, + ["Fatal"] = LogLevel.Critical, + ["Off"] = LogLevel.None + }; + + /// + /// Creates a new instance of the + /// class. + /// + public LogLevelConverter() : base(typeof(LogLevel)) + { + } + + /// + /// Convert from a string value to a instance. + /// + /// + /// A + /// that provides a format context. + /// + /// + /// The to use + /// as the current culture. + /// + /// + /// The value that is to be converted. + /// + /// + /// A if successful. + /// + 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); + } +} diff --git a/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs b/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs index a6d567f84..6e2051fec 100644 --- a/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs +++ b/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs @@ -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; @@ -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(); diff --git a/src/Spring/Spring.Core/LogManager.cs b/src/Spring/Spring.Core/LogManager.cs index 02bd9d955..73ef74237 100644 --- a/src/Spring/Spring.Core/LogManager.cs +++ b/src/Spring/Spring.Core/LogManager.cs @@ -24,6 +24,12 @@ public static class LogManager /// /// Gets or sets the current log provider based on logger factory. /// + /// + /// Until a factory is assigned, all loggers returned by the GetLogger methods + /// are no-op instances. Assigning a factory takes effect for + /// all loggers resolved afterwards, including lazily resolved ones such as those used + /// by Spring.Aspects.Logging.AbstractLoggingAdvice. + /// public static ILoggerFactory LoggerFactory { get; set; } public static ILogger GetLogger(string category) => LoggerFactory?.CreateLogger(category) ?? NullLogger.Instance; diff --git a/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs b/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs index 1d9055332..20607e4cf 100644 --- a/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs +++ b/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs @@ -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; @@ -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] @@ -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 = $@" + + + + +"; + + 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 Categories { get; } = []; + public List 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 messages) : ILogger + { + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + messages.Add(formatter(state, exception)); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + } + } } public class Dog diff --git a/test/Spring/Spring.Core.Tests/Core/TypeConversion/LogLevelConverterTests.cs b/test/Spring/Spring.Core.Tests/Core/TypeConversion/LogLevelConverterTests.cs new file mode 100644 index 000000000..3bcab5c44 --- /dev/null +++ b/test/Spring/Spring.Core.Tests/Core/TypeConversion/LogLevelConverterTests.cs @@ -0,0 +1,73 @@ +/* + * 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 Microsoft.Extensions.Logging; +using NUnit.Framework; + +namespace Spring.Core.TypeConversion; + +/// +/// Unit tests for the LogLevelConverter class. +/// +[TestFixture] +public sealed class LogLevelConverterTests +{ + private readonly LogLevelConverter converter = new LogLevelConverter(); + + [TestCase("All", LogLevel.Trace)] + [TestCase("Info", LogLevel.Information)] + [TestCase("info", LogLevel.Information)] + [TestCase(" Info ", LogLevel.Information)] + [TestCase("Warn", LogLevel.Warning)] + [TestCase("WARN", LogLevel.Warning)] + [TestCase("Fatal", LogLevel.Critical)] + [TestCase("Off", LogLevel.None)] + public void ConvertsLegacyCommonLoggingLevelNames(string text, LogLevel expected) + { + Assert.That(converter.ConvertFrom(text), Is.EqualTo(expected)); + } + + [TestCase("Trace", LogLevel.Trace)] + [TestCase("Debug", LogLevel.Debug)] + [TestCase("Information", LogLevel.Information)] + [TestCase("information", LogLevel.Information)] + [TestCase("Warning", LogLevel.Warning)] + [TestCase("Error", LogLevel.Error)] + [TestCase("Critical", LogLevel.Critical)] + [TestCase("None", LogLevel.None)] + public void ConvertsCanonicalLevelNames(string text, LogLevel expected) + { + Assert.That(converter.ConvertFrom(text), Is.EqualTo(expected)); + } + + [Test] + public void ConvertsNumericStrings() + { + Assert.That(converter.ConvertFrom("2"), Is.EqualTo(LogLevel.Information)); + } + + [Test] + public void ThrowsFormatExceptionForUnknownLevelName() + { + Assert.Throws(() => converter.ConvertFrom("Verbose")); + } + + [Test] + public void CanConvertFromString() + { + Assert.IsTrue(converter.CanConvertFrom(typeof(string))); + } +} diff --git a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs index 0653df176..c1b94632f 100644 --- a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs +++ b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Microsoft.Extensions.Logging; using NUnit.Framework; namespace Spring.Core.TypeConversion; @@ -64,4 +65,17 @@ public void ConvertValueWithDutchCultureForDecimalMarkWithCommaReturnsValue() object o = TypeConversionUtils.ConvertValueIfNecessary(typeof(Double), "1,2", "foo"); Assert.That(o, Is.EqualTo(1.2)); } + + [Test] + public void ConvertsLegacyCommonLoggingLogLevelName() + { + object o = TypeConversionUtils.ConvertValueIfNecessary(typeof(LogLevel), "Info", "LogLevel"); + Assert.That(o, Is.EqualTo(LogLevel.Information)); + } + + [Test] + public void ThrowsTypeMismatchForUnknownLogLevelName() + { + Assert.Throws(() => TypeConversionUtils.ConvertValueIfNecessary(typeof(LogLevel), "Verbose", "LogLevel")); + } } diff --git a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs index 1346c05ab..395b5d41f 100644 --- a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs +++ b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs @@ -33,6 +33,13 @@ public void GetConverterForEnums() Assert.IsTrue(converter is EnumConverter); } + [Test] + public void GetConverterForLogLevel() + { + TypeConverter converter = TypeConverterRegistry.GetConverter(typeof(Microsoft.Extensions.Logging.LogLevel)); + Assert.IsTrue(converter is LogLevelConverter); + } + [Test] public void GetInternalConverter() {