You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by fr...@apache.org on 2026/08/19 20:48:34 UTC

(logging-log4net) branch Feature/162-quiet-app-settings-in-a-native-host created (now 633d0288)

This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a change to branch Feature/162-quiet-app-settings-in-a-native-host
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git


      at 633d0288 Document the pattern layout tests

This branch includes the following new commits:

     new 13d931ef Recognise a native host as having no configuration system (#162)
     new 24457a61 Guard the native host case in CI (#162)
     new 633d0288 Document the pattern layout tests

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4net) 03/03: Document the pattern layout tests

Posted by fr...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a commit to branch Feature/162-quiet-app-settings-in-a-native-host
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit 633d0288a1da41c937a2ac2f244f015a6ccb3cde
Author: Jan Friedrich <fr...@apache.org>
AuthorDate: Wed Aug 19 22:42:56 2026 +0200

    Document the pattern layout tests
    
    Every test method, the fixture hooks and the layout factories now carry a
    summary saying what they assert, as does the converter that feeds arbitrary
    names to NamedPatternConverter. The factories note that
    DynamicPatternLayoutTest inherits this fixture and reruns the whole suite
    against a DynamicPatternLayout, which is not obvious from either file.
    
    The conditional using directive that came with it keeps System.Linq out of the
    net462 compilation, where nothing uses it, which clears an IDE0005 warning.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 src/log4net.Tests/Layout/PatternLayoutTest.cs | 68 +++++++++++++++++++++++++--
 1 file changed, 65 insertions(+), 3 deletions(-)

diff --git a/src/log4net.Tests/Layout/PatternLayoutTest.cs b/src/log4net.Tests/Layout/PatternLayoutTest.cs
index 81e15d6d..da7cc6e5 100644
--- a/src/log4net.Tests/Layout/PatternLayoutTest.cs
+++ b/src/log4net.Tests/Layout/PatternLayoutTest.cs
@@ -21,7 +21,9 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
+#if NET8_0_OR_GREATER
 using System.Linq;
+#endif
 using System.Threading;
 using log4net.Config;
 using log4net.Core;
@@ -37,15 +39,16 @@ namespace log4net.Tests.Layout;
 /// <summary>
 /// Used for internal unit testing the <see cref="PatternLayout"/> class.
 /// </summary>
-/// <remarks>
-/// Used for internal unit testing the <see cref="PatternLayout"/> class.
-/// </remarks>
 [TestFixture]
 public class PatternLayoutTest
 {
   private CultureInfo? _currentCulture;
   private CultureInfo? _currentUiCulture;
 
+  /// <summary>
+  /// Renders in the invariant culture, so that the dates and numbers the tests assert on do not
+  /// depend on the machine's locale.
+  /// </summary>
   [SetUp]
   public void SetUp()
   {
@@ -54,6 +57,10 @@ public void SetUp()
     _currentUiCulture = Thread.CurrentThread.CurrentUICulture;
     Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
   }
+
+  /// <summary>
+  /// Removes the context property the tests set and restores the culture <see cref="SetUp"/> replaced.
+  /// </summary>
   [TearDown]
   public void TearDown()
   {
@@ -63,10 +70,23 @@ public void TearDown()
     Thread.CurrentThread.CurrentUICulture = _currentUiCulture!;
   }
 
+  /// <summary>
+  /// Creates the layout every test in this fixture renders with.
+  /// </summary>
+  /// <returns>the layout under test</returns>
   protected virtual PatternLayout NewPatternLayout() => new();
 
+  /// <summary>
+  /// Creates the layout every test in this fixture renders with, from a conversion pattern.
+  /// </summary>
+  /// <param name="pattern">the conversion pattern</param>
+  /// <returns>the layout under test</returns>
   protected virtual PatternLayout NewPatternLayout(string pattern) => new(pattern);
 
+  /// <summary>
+  /// %property{key} renders a value taken from <see cref="ThreadContext"/>, and the null text while
+  /// the property is unset or has been removed again.
+  /// </summary>
   [Test]
   public void TestThreadPropertiesPattern()
   {
@@ -97,6 +117,9 @@ public void TestThreadPropertiesPattern()
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// %stacktrace{2} names the method that logged the event.
+  /// </summary>
   [Test]
   public void TestStackTracePattern()
   {
@@ -115,6 +138,10 @@ public void TestStackTracePattern()
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// %property{key} renders a value taken from <see cref="GlobalContext"/>, and the null text while
+  /// the property is unset or has been removed again.
+  /// </summary>
   [Test]
   public void TestGlobalPropertiesPattern()
   {
@@ -145,6 +172,10 @@ public void TestGlobalPropertiesPattern()
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// A converter registered with <c>AddConverter</c> is picked up by its name in the conversion
+  /// pattern.
+  /// </summary>
   [Test]
   public void TestAddingCustomPattern()
   {
@@ -167,6 +198,11 @@ public void TestAddingCustomPattern()
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// A <see cref="NamedPatternConverter"/> without a precision renders the whole name, dots and all.
+  /// The empty string, a bare dot and a leading or trailing dot are covered, because those are where
+  /// an off by one in the index arithmetic would show.
+  /// </summary>
   [Test]
   public void NamedPatternConverterWithoutPrecisionShouldReturnFullName()
   {
@@ -214,6 +250,10 @@ public void NamedPatternConverterWithoutPrecisionShouldReturnFullName()
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// A precision of 1 keeps the last dot separated component, and leaves a name that ends in a dot,
+  /// or has no dot at all, as it is.
+  /// </summary>
   [Test]
   public void NamedPatternConverterWithPrecision1ShouldStripLeadingStuffIfPresent()
   {
@@ -261,6 +301,9 @@ public void NamedPatternConverterWithPrecision1ShouldStripLeadingStuffIfPresent(
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// A precision of 2 keeps the last two dot separated components.
+  /// </summary>
   [Test]
   public void NamedPatternConverterWithPrecision2ShouldStripLessLeadingStuffIfPresent()
   {
@@ -323,6 +366,9 @@ private sealed class TestMessagePatternConverter : PatternLayoutConverter
     protected override void Convert(TextWriter writer, LoggingEvent loggingEvent) => loggingEvent.WriteRenderedMessage(writer);
   }
 
+  /// <summary>
+  /// %exception{stacktrace} renders the null text rather than the stack trace of the logged exception.
+  /// </summary>
   [Test]
   public void TestExceptionPattern()
   {
@@ -343,6 +389,10 @@ public void TestExceptionPattern()
     stringAppender.Reset();
   }
 
+  /// <summary>
+  /// Two %utcdate converters in one pattern each render in their own format, and both follow the
+  /// timestamp of the event rather than reusing what they rendered for the previous one.
+  /// </summary>
   [Test]
   public void ConvertMultipleDatePatternsTest()
   {
@@ -364,6 +414,9 @@ public void ConvertMultipleDatePatternsTest()
   }
 
 #if NET8_0_OR_GREATER
+  /// <summary>
+  /// A date format with microsecond precision renders all six fractional digits.
+  /// </summary>
   [Test]
   public void ConvertMicrosecondsPatternTest()
   {
@@ -381,6 +434,10 @@ public void ConvertMicrosecondsPatternTest()
     Assert.That(stringAppender.GetString(), Is.EqualTo("20250210 13:01:02.123456"));
   }
 
+  /// <summary>
+  /// Microsecond timestamps stay distinct across events logged in a tight loop, so no part of the
+  /// rendered date is cached between events.
+  /// </summary>
   [Test]
   public void ConvertMultipleMicrosecondsPatternTest()
   {
@@ -404,9 +461,14 @@ public void ConvertMultipleMicrosecondsPatternTest()
   }
 #endif
 
+  /// <summary>
+  /// Converter that treats the message of an event as the name a <see cref="NamedPatternConverter"/>
+  /// works on, so that the precision handling can be exercised with arbitrary input.
+  /// </summary>
   [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Reflection")]
   private sealed class MessageAsNamePatternConverter : NamedPatternConverter
   {
+    /// <inheritdoc/>
     protected override string GetFullyQualifiedName(LoggingEvent loggingEvent) => loggingEvent.MessageObject?.ToString() ?? string.Empty;
   }
 }
\ No newline at end of file


(logging-log4net) 02/03: Guard the native host case in CI (#162)

Posted by fr...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a commit to branch Feature/162-quiet-app-settings-in-a-native-host
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit 24457a61a1597541595a1b374af7c3daa61dd752
Author: Jan Friedrich <fr...@apache.org>
AuthorDate: Wed Aug 19 22:42:48 2026 +0200

    Guard the native host case in CI (#162)
    
    The unit test for a natively hosted process builds the exception by hand, so it
    asserts what we believe the configuration system does rather than what it does.
    This adds a check that removes the entry assembly for real and reports what
    log4net writes while starting up, in the project that already exists to record
    what log4net can do in an unusual host.
    
    It cannot be one of the probes in the list: the configuration system caches its
    initialization and log4net reads its first application setting from a static
    constructor, so by the time any probe runs the outcome is already decided. The
    check therefore owns the process from its first statement and the runner selects
    it with an argument.
    
    Verified in both directions: it passes as committed, and reverting the
    PlatformNotSupportedException case makes it exit non-zero and print the seven
    error blocks it captured.
    
    JIT compiled only, deliberately. Native AOT trims the configuration system away,
    so the first setting read there fails as a missing constructor long before it can
    fail for want of an entry assembly: the published executable passes this check
    even with the fix reverted, which would be a green that cannot fail for the
    reason the check exists. The reason is recorded next to the CI step and on the
    class, so it does not get added back for symmetry.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 .github/workflows/build.yaml             |  16 +++
 src/log4net.Tests.Aot/NativeHostCheck.cs | 189 +++++++++++++++++++++++++++++++
 src/log4net.Tests.Aot/Program.cs         |  10 +-
 3 files changed, 214 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 2599ffcd..af5b3359 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -72,4 +72,20 @@ jobs:
             dotnet publish ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -o ./aot-probes
             if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
             & "./aot-probes/log4net.Tests.Aot$($IsWindows ? '.exe' : '')"
+            if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+      # A process that hosts the runtime natively has no entry assembly, so the configuration
+      # system cannot work out where the config file is (issue #162). The check owns the process
+      # from its first statement, because the configuration system caches its initialization and
+      # log4net reads its first setting from a static constructor, so it cannot be one of the
+      # probes above.
+      #
+      # JIT only, deliberately: under Native AOT the configuration system is trimmed away, so the
+      # first setting read fails as a missing constructor long before it can fail for want of an
+      # entry assembly. The published executable passes this check with the fix reverted, which
+      # makes it a green that cannot fail for the reason the check exists.
+      - name: AOT probes, no entry assembly
+        shell: pwsh
+        run: |
+            dotnet run --project ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -- --no-entry-assembly
             if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
\ No newline at end of file
diff --git a/src/log4net.Tests.Aot/NativeHostCheck.cs b/src/log4net.Tests.Aot/NativeHostCheck.cs
new file mode 100644
index 00000000..d799ffea
--- /dev/null
+++ b/src/log4net.Tests.Aot/NativeHostCheck.cs
@@ -0,0 +1,189 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you 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.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+using log4net.Appender;
+using log4net.Config;
+using log4net.Core;
+using log4net.Layout;
+using log4net.Util;
+
+namespace log4net.Tests.Aot;
+
+/// <summary>
+/// Records what log4net does in a process that hosts the runtime natively, where there is no entry
+/// assembly for the configuration system to derive the config file path from (issue #162).
+/// </summary>
+/// <remarks>
+/// <para>
+/// This cannot be one of <see cref="Probes"/>: the configuration system caches its initialization,
+/// and log4net reads its first application setting from a static constructor, so by the time any
+/// probe runs the outcome is already decided. The check therefore owns the process from its first
+/// statement, which is why the runner invokes it instead of the probe list.
+/// </para>
+/// <para>
+/// Removing the entry assembly is what a native host does to the process, and reflecting onto
+/// <c>Assembly.SetEntryAssembly</c> is the only way to arrive there without one. It is a CoreCLR
+/// internal, so the check reports itself as not applicable rather than failing where it is absent.
+/// </para>
+/// <para>
+/// The build runs this JIT compiled only. Native AOT trims the configuration system away, so there
+/// the first setting read fails as a missing constructor before it can fail for want of an entry
+/// assembly: the published executable passes this check even with the fix for #162 reverted, which
+/// would be a green that cannot fail for the reason the check exists.
+/// </para>
+/// </remarks>
+internal static class NativeHostCheck
+{
+  /// <summary>
+  /// The argument that selects this check instead of the probe list.
+  /// </summary>
+  internal const string Argument = "--no-entry-assembly";
+
+  /// <summary>
+  /// A setting that only app.config carries, so reading it proves whether the configuration system
+  /// was really bypassed rather than answering from a cache.
+  /// </summary>
+  private const string ConfigOnlyKey = "log4net.AotProbe";
+
+  /// <summary>
+  /// Removes the entry assembly, starts log4net, and reports what it wrote while doing so.
+  /// </summary>
+  /// <returns>0 if log4net started without reporting an error, otherwise 1</returns>
+  internal static int Run()
+  {
+    Console.WriteLine("log4net probes, no entry assembly");
+    Console.WriteLine(new string('-', 100));
+
+    if (RemoveTheEntryAssembly() is string unavailable)
+    {
+      Console.WriteLine($"  {"n/a",-6} settings/no error without an entry assembly");
+      Console.WriteLine($"         {unavailable}");
+      Console.WriteLine(new string('-', 100));
+      Console.WriteLine("no entry assembly: not applicable here");
+      return 0;
+    }
+
+    // log4net reports an unreadable configuration system on Console.Error, from a static
+    // constructor, so the writer has to be in place before anything touches log4net at all.
+    TextWriter console = Console.Error;
+    using StringWriter emitted = new();
+    Console.SetError(emitted);
+    List<string> failures = [];
+    try
+    {
+      Environment.SetEnvironmentVariable(Program.EnvironmentProbeKey, "from-environment");
+      Check(failures, SystemInfo.GetAppSetting(Program.EnvironmentProbeKey) == "from-environment",
+        "an application setting was not read from the environment");
+      Check(failures, SystemInfo.GetAppSetting(ConfigOnlyKey) is null,
+        $"{ConfigOnlyKey} was answered from app.config, so this check exercised nothing");
+      Check(failures, Log() == 1, "the event did not reach the appender");
+    }
+    catch (Exception e) when (e is not (OutOfMemoryException or StackOverflowException))
+    {
+      failures.Add($"{e.GetType().Name}: {e.Message}");
+    }
+    finally
+    {
+      Console.SetError(console);
+    }
+
+    // The symptom of #162 is a stack trace for every setting log4net reads, in an application whose
+    // configuration is fine. An absent configuration system is a property of the host, so it
+    // belongs at debug level, and nothing here turns internal debugging on.
+    string output = emitted.ToString();
+    Check(failures, !output.Contains("log4net:ERROR", StringComparison.Ordinal),
+      "log4net reported an error while reading its application settings");
+
+    foreach (string failure in failures)
+    {
+      Console.WriteLine($"  {"FAIL",-6} settings/no error without an entry assembly");
+      Console.WriteLine($"         {failure}");
+    }
+    if (failures.Count == 0)
+    {
+      Console.WriteLine($"  {"ok",-6} settings/no error without an entry assembly");
+    }
+    else if (output.Length > 0)
+    {
+      Console.WriteLine("what log4net wrote:");
+      Console.WriteLine(output);
+    }
+
+    Console.WriteLine(new string('-', 100));
+    Console.WriteLine(failures.Count == 0
+      ? "no entry assembly: log4net started without reporting an error"
+      : $"no entry assembly: {failures.Count} check(s) did not match expectations");
+    return failures.Count == 0 ? 0 : 1;
+  }
+
+  /// <summary>
+  /// Makes this process look like a natively hosted one.
+  /// </summary>
+  /// <returns>null once there is no entry assembly, otherwise why that cannot be arranged</returns>
+  private static string? RemoveTheEntryAssembly()
+  {
+    MethodInfo? setEntryAssembly = typeof(Assembly)
+      .GetMethod("SetEntryAssembly", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+    if (setEntryAssembly is null)
+    {
+      return "Assembly.SetEntryAssembly is not available, so the entry assembly cannot be removed";
+    }
+    try
+    {
+      setEntryAssembly.Invoke(null, [null]);
+    }
+    catch (Exception e) when (e is not (OutOfMemoryException or StackOverflowException))
+    {
+      return $"Assembly.SetEntryAssembly rejected the call: {e.InnerException?.Message ?? e.Message}";
+    }
+    return Assembly.GetEntryAssembly() is null
+      ? null
+      : "Assembly.SetEntryAssembly left an entry assembly in place";
+  }
+
+  /// <summary>
+  /// Starts log4net the way an application would, which is what reads the application settings.
+  /// </summary>
+  /// <returns>the number of events that reached the appender</returns>
+  private static int Log()
+  {
+    MemoryAppender memory = new()
+    {
+      Layout = new PatternLayout("%level %logger %message"),
+      Threshold = Level.All,
+    };
+    memory.ActivateOptions();
+    BasicConfigurator.Configure(memory);
+    LogManager.GetLogger(typeof(NativeHostCheck)).Info("hello from a host without an entry assembly");
+    return memory.GetEvents().Length;
+  }
+
+  private static void Check(List<string> failures, bool condition, string message)
+  {
+    if (!condition)
+    {
+      failures.Add(message);
+    }
+  }
+}
diff --git a/src/log4net.Tests.Aot/Program.cs b/src/log4net.Tests.Aot/Program.cs
index 5f484cb4..bbe262cb 100644
--- a/src/log4net.Tests.Aot/Program.cs
+++ b/src/log4net.Tests.Aot/Program.cs
@@ -19,6 +19,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
 
 using log4net.Util;
@@ -42,8 +43,15 @@ internal static class Program
   /// </summary>
   internal const string EnvironmentProbeKey = "log4net.AotEnvironmentProbe";
 
-  private static int Main()
+  private static int Main(string[] args)
   {
+    if (args.Contains(NativeHostCheck.Argument))
+    {
+      // Owns the process from here: it has to remove the entry assembly before anything reads a
+      // setting, so it cannot share a run with the probes.
+      return NativeHostCheck.Run();
+    }
+
     // The probes report their own failures, so log4net's internal error reporting is only noise
     // here, and a passing run that prints errors reads as a broken one.
     LogLog.EmitInternalMessages = false;


(logging-log4net) 01/03: Recognise a native host as having no configuration system (#162)

Posted by fr...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

FreeAndNil pushed a commit to branch Feature/162-quiet-app-settings-in-a-native-host
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit 13d931ef36ecc607786befc4c4cee2226a52ffc8
Author: Jan Friedrich <fr...@apache.org>
AuthorDate: Wed Aug 19 22:12:52 2026 +0200

    Recognise a native host as having no configuration system (#162)
    
    In a process that hosts the runtime natively there is no entry assembly for the
    configuration system to derive the config file path from, so reading an
    application setting fails in ClientConfigPaths with a PlatformNotSupportedException
    wrapped in a ConfigurationErrorsException. That matched none of the shapes
    IsMissingConfigurationSystem recognised, so log4net blamed the user's config file
    and repeated the report for every setting the static constructors read: seven
    log4net:ERROR blocks with a stack trace each, at startup, in a well configured
    application.
    
    A PlatformNotSupportedException anywhere in the inner exception chain is now
    treated as an absent configuration system, the same as the Native AOT case. A
    malformed config file cannot produce one, so unlike the FileNotFoundException and
    TypeLoadException cases it needs no check on which assembly it came from. The
    failure is logged once at debug level and application settings come from
    environment variables.
    
    Verified on Windows in a C++ host of the CoreCLR built for the purpose, in a host
    that loads the runtime through hostfxr, and in powershell.exe loading a
    netstandard2.0 build output: seven error blocks before, none after, and exactly one
    debug line with log4net.Internal.Debug on.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 .../162-quiet-app-settings-in-a-native-host.xml    | 14 ++++++++++++++
 src/log4net.Tests/Util/SystemInfoTest.cs           | 13 +++++++++++++
 src/log4net/Util/SystemInfo.cs                     | 22 +++++++++++++++-------
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/src/changelog/3.4.1/162-quiet-app-settings-in-a-native-host.xml b/src/changelog/3.4.1/162-quiet-app-settings-in-a-native-host.xml
new file mode 100644
index 00000000..9f995548
--- /dev/null
+++ b/src/changelog/3.4.1/162-quiet-app-settings-in-a-native-host.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <issue id="162" link="https://github.com/apache/logging-log4net/issues/162"/>
+  <description format="asciidoc">Stop reporting `log4net:ERROR Exception while reading ConfigurationSettings`
+  in a process that hosts the runtime natively, such as `powershell.exe` or a C++ host of the CoreCLR.
+  There is no entry assembly there for the configuration system to derive the config file path from, so
+  it fails with `PlatformNotSupportedException` before any config file is read. That is now recognised
+  as an absent configuration system, the same as under Native AOT: it is logged once at debug level and
+  application settings are read from environment variables instead
+  (reported by @viktorgobbi, fixed by @FreeAndNil)</description>
+</entry>
diff --git a/src/log4net.Tests/Util/SystemInfoTest.cs b/src/log4net.Tests/Util/SystemInfoTest.cs
index fbf544bd..ebf3a72f 100644
--- a/src/log4net.Tests/Util/SystemInfoTest.cs
+++ b/src/log4net.Tests/Util/SystemInfoTest.cs
@@ -274,6 +274,19 @@ public void MissingApplicationAssemblyIsNotTreatedAsAMissingConfigurationSystem(
       new ConfigurationErrorsException("An error occurred creating the configuration section handler",
         new FileNotFoundException("Could not load file or assembly", "Contoso.SectionHandlers"))), Is.False);
 
+  /// <summary>
+  /// A process that hosts the runtime natively has no entry assembly for the configuration system
+  /// to derive the config file path from, so it cannot read a config file whatever state that file
+  /// is in. That is recognised and the environment stands in, rather than being reported as a
+  /// malformed file on every setting log4net reads.
+  /// </summary>
+  [Test]
+  public void NativeHostExceptionIsRecognised()
+    => Assert.That(IsMissingConfigurationSystem(
+        new ConfigurationErrorsException("Configuration system failed to initialize",
+          new PlatformNotSupportedException("Operation is not supported on this platform."))),
+      Is.True);
+
   private static bool IsMissingConfigurationSystem(Exception exception)
   {
     MethodInfo method = typeof(SystemInfo).GetMethod("IsMissingConfigurationSystem", BindingFlags.Static | BindingFlags.NonPublic)
diff --git a/src/log4net/Util/SystemInfo.cs b/src/log4net/Util/SystemInfo.cs
index eb3c656b..f71876d7 100644
--- a/src/log4net/Util/SystemInfo.cs
+++ b/src/log4net/Util/SystemInfo.cs
@@ -721,12 +721,13 @@ public static bool TryParse(string s, out short val)
     {
       if (IsMissingConfigurationSystem(e))
       {
-        // There is no configuration system to read - Native AOT trims System.Configuration away.
-        // That is a property of the runtime rather than a fault, so it is not reported as an
-        // error, and the environment stands in for the config file as it does on Android.
+        // There is no configuration system to read - Native AOT trims System.Configuration away,
+        // and a native process hosting the runtime has no entry assembly for it to derive the
+        // config file path from. That is a property of the host rather than a fault, so it is not
+        // reported as an error, and the environment stands in for the config file as on Android.
         _configurationSystemUnavailable = true;
         LogLog.Debug(_declaringType,
-          "No configuration system on this runtime. Using environment variables for application settings.", e);
+          "No configuration system on this host. Using environment variables for application settings.", e);
         return Environment.GetEnvironmentVariable(key);
       }
 
@@ -746,10 +747,12 @@ public static bool TryParse(string s, out short val)
   /// <returns><see langword="true"/> if the configuration system itself is unavailable</returns>
   /// <remarks>
   /// <para>
-  /// The inner exceptions have to be walked, because Native AOT surfaces this as a
+  /// The inner exceptions have to be walked, because the runtime surfaces both cases as a
   /// <see cref="ConfigurationErrorsException"/> - the very type a malformed file produces. What
-  /// distinguishes it is further down the chain: a <see cref="MissingMethodException"/> for
-  /// <c>ClientConfigurationHost</c>, whose constructor the trimmer removed.
+  /// distinguishes them is further down the chain: under Native AOT a
+  /// <see cref="MissingMethodException"/> for <c>ClientConfigurationHost</c>, whose constructor the
+  /// trimmer removed, and in a native process hosting the runtime a
+  /// <see cref="PlatformNotSupportedException"/> from <c>ClientConfigPaths</c>.
   /// </para>
   /// <para>
   /// An unrecognized failure is treated as a configuration file problem, which is the safer way
@@ -773,6 +776,11 @@ private static bool IsMissingConfigurationSystem(Exception? exception)
     {
       switch (exception)
       {
+        // The configuration system cannot work out where the config file is, because there is no
+        // entry assembly to derive its path from. That is what a native process hosting the runtime
+        // looks like, and no config file can be read there however well formed it is. A malformed
+        // file never produces this, so it needs no check on which assembly it came from.
+        case PlatformNotSupportedException:
         case FileNotFoundException { FileName: string fileName }
           when IsConfigurationSystem(fileName):
         case TypeLoadException { TypeName: string typeName }