Apache log4net 3.5.0 集成使用实战详解

基于Ubuntu 22.04实测log4net 3.5.0从dotnet环境搭建、NuGet引入到控制台与滚动文件示例的完整实战。

Apache log4netlog4netApache.NET日志2026/10/15

Apache log4net 3.5.0 集成使用实战详解

目标:在 Ubuntu 22.04 上从零搭建 .NET 8 环境,NuGet 引入 log4net 3.5.0,完成第一个日志程序、Logger 层次与 Filter 实战、滚动文件压测与内部排障。全程命令与回显已在 Ubuntu 22.04 实测通过。

一、环境信息

项目说明
操作系统Ubuntu 22.04.5 LTS
架构x86_64
运行时.NET SDK 8.0.426(dotnet-install.sh 安装至 /usr/local/dotnet)
组件版本log4net 3.5.0(NuGet)
目录项目 ~/dotnetdemo/Log4NetDemo,日志输出 ~/dotnetdemo/Log4NetDemo/bin/Debug/net8.0/logs

逐项说明:root 账号执行;NuGet 恢复走 api.nuget.org,网络受限时可为 dotnet 进程导出代理环境变量;下文所有 dotnet 命令均先执行 export PATH=/usr/local/dotnet:$PATH。

二、前置依赖

2.1 安装基础工具

apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y wget unzip ca-certificates

实测回显尾部:

Setting up unzip (6.0-26ubuntu3.2) ...
Processing triggers for ca-certificates (20260601~22.04.1) ...
Updating certificates in /etc/ssl/certs...
0 added, 0 removed; done.

2.2 安装 .NET SDK 8.0

Ubuntu 22.04 官方仓库没有 dotnet-sdk-8.0 的最新版,采用微软 dotnet-install.sh 脚本安装(网络受限时导出代理):

export http_proxy=http://192.168.1.62:7897 https_proxy=http://192.168.1.62:7897
wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh
chmod +x /tmp/dotnet-install.sh
bash /tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/local/dotnet

实测回显(节选):

dotnet-install: Adding to current process PATH: `/usr/local/dotnet`. Note: This change will be visible only when sourcing script.
dotnet-install: Note that the script does not resolve dependencies during installation.
dotnet-install: To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the "Dependencies" section.
dotnet-install: Installation finished successfully.

验证版本:

export PATH=/usr/local/dotnet:$PATH
dotnet --version

实测回显:

8.0.426

逐项解说:--install-dir 指定安装目录后需手动把该目录加入 PATH;脚本方式不写系统包管理器记录,升级重跑即可;多实例共存时用 DOTNET_ROOT 环境变量辅助定位。

三、NuGet 引入 log4net 3.5.0

3.1 创建控制台项目

mkdir -p ~/dotnetdemo && cd ~/dotnetdemo
dotnet new console -n Log4NetDemo -o Log4NetDemo --framework net8.0

实测回显(节选):

Restore succeeded.

Post action required:
Description: If you're using on Linux...

Welcome to .NET 8.0!
--------------------
Thanks for installing .NET SDK 8.0.426 and Terminal Logger.

3.2 添加 log4net 包

cd ~/dotnetdemo/Log4NetDemo
export http_proxy=http://192.168.1.62:7897 https_proxy=http://192.168.1.62:7897
dotnet add package log4net --version 3.5.0

实测回显(节选):

info : Package 'log4net' is compatible with all the specified frameworks in project '/root/dotnetdemo/Log4NetDemo/Log4NetDemo.csproj'.
info : PackageReference for package 'log4net' version '3.5.0' added to file '/root/dotnetdemo/Log4NetDemo/Log4NetDemo.csproj'.
log  : Restored /root/dotnetdemo/Log4NetDemo/Log4NetDemo.csproj (in 5.75 sec).

验证项目文件:

cat Log4NetDemo.csproj

实测回显:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="log4net" Version="3.5.0" />
  </ItemGroup>
</Project>

逐项解说:PackageReference 指定 3.5.0 锁定版本;无其它依赖项,log4net 零第三方传递依赖。

四、第一个日志程序(控制台 + 滚动文件)

4.1 配置文件 log4net.config

<?xml version="1.0" encoding="utf-8"?>
<log4net>
  <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
    </layout>
  </appender>
  <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
    <file value="logs/app.log" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="3" />
    <maximumFileSize value="1MB" />
    <staticLogFileName value="true" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
    </layout>
  </appender>
  <root>
    <level value="DEBUG" />
    <appender-ref ref="ConsoleAppender" />
    <appender-ref ref="RollingFileAppender" />
  </root>
</log4net>

逐项解说:root 级别 DEBUG,挂控制台与滚动文件两个 Appender;rollingStyle=Size 按大小滚动,maximumFileSize=1MB,maxSizeRollBackups=3 保留 3 份历史;staticLogFileName=true 表示滚动发生在写满当前文件时。

4.2 Program.cs

// 文件:~/dotnetdemo/Log4NetDemo/Program.cs
using System;
using System.IO;
using log4net;
using log4net.Config;
using log4net.Repository;

namespace Log4NetDemo
{
    class Program
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(Program));

        static void Main(string[] args)
        {
            ILoggerRepository repository = LogManager.GetRepository(System.Reflection.Assembly.GetEntryAssembly()!);
            XmlConfigurator.Configure(new FileInfo("log4net.config"));

            log.Debug("调试信息:订单服务启动参数加载完成");
            log.Info("普通信息:用户 admin 于 2026-10-15 登录系统");
            log.Warn("警告信息:磁盘使用率已达 82%");
            log.Error("错误信息:连接数据库失败,重试第 1 次", new Exception("Connection refused"));
            log.Fatal("严重信息:服务进程即将退出");

            Console.WriteLine("日志输出完毕,全局 Threshold = " + repository.Threshold.Name);
        }
    }
}

💡 技术拓展:GetLogger 的推荐姿势是传入 typeof(Program)(Logger 名=类型全名),也可传字符串常量;静态 readonly ILog 字段只创建一次,是 log4net 官方范式。此处先显式读出 Repository Threshold 演示仓库概念,业务代码可省略这两行。

4.3 首次编译踩坑实录

直接运行:

export PATH=/usr/local/dotnet:$PATH
dotnet run

实测报错(排障过程):

/root/dotnetdemo/Log4NetDemo/Program.cs(24,79): error CS1061: 'Level' does not contain a definition for 'Level' and no accessible extension method 'Level' accepting a first argument of type 'Level' could be found (are you missing a using directive or an assembly reference?) [/root/dotnetdemo/Log4NetDemo/Log4NetDemo.csproj]
/root/dotnetdemo/Log4NetDemo/Program.cs(15,69): warning CS8604: Possible null reference argument for parameter 'repositoryAssembly' in 'ILoggerRepository LogManager.GetRepository(Assembly repositoryAssembly)'. [/root/dotnetdemo/Log4NetDemo/Log4NetDemo.csproj]

排障定位:repository.Threshold 的类型本身就是 log4net.Core.Level,取文本要用 .Name 而不是 .Level;GetEntryAssembly() 在 Nullable 上下文下返回可能为 null,加 ! 包容。修正后重跑:

dotnet run

实测输出:

2026-10-14 16:50:46,312 [1] DEBUG Log4NetDemo.Program - 调试信息:订单服务启动参数加载完成
2026-10-14 16:50:46,322 [1] INFO  Log4NetDemo.Program - 普通信息:用户 admin 于 2026-10-15 登录系统
2026-10-14 16:50:46,323 [1] WARN  Log4NetDemo.Program - 警告信息:磁盘使用率已达 82%
2026-10-14 16:50:46,323 [1] ERROR Log4NetDemo.Program - 错误信息:连接数据库失败,重试第 1 次
System.Exception: Connection refused
2026-10-14 16:50:46,324 [1] FATAL Log4NetDemo.Program - 严重信息:服务进程即将退出
日志输出完毕,全局 Threshold = ALL

逐项解说:五个级别全部输出(root=DEBUG);Error 调用携带的异常在消息后自动打印堆栈首行(System.Exception: Connection refused);[%thread] 在 Linux 下为主线程编号 1;Threshold 输出 ALL 表示仓库级未设总闸。

4.4 验证文件落盘

find ~/dotnetdemo/Log4NetDemo -name 'app.log' -exec head -1 {} \;

实测回显:

2026-10-14 16:50:46,312 [1] DEBUG Log4NetDemo.Program - 调试信息:订单服务启动参数加载完成

逐项解说:文件位于 bin/Debug/net8.0/logs/app.log——相对路径 logs/app.log 的基准是应用程序基目录而非 shell 当前目录(详见 FAQ Q3)。

五、进阶实战

5.1 Logger 层次继承与 Filter

配置文件 hierarchy.config:

<?xml version="1.0" encoding="utf-8"?>
<log4net>
  <appender name="Console" type="log4net.Appender.ConsoleAppender">
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="[%-5level] %logger{1} - %message%newline" />
    </layout>
    <filter type="log4net.Filter.LevelRangeFilter">
      <levelMin value="DEBUG" />
      <levelMax value="ERROR" />
    </filter>
  </appender>
  <logger name="OrderService.Dao">
    <level value="INFO" />
  </logger>
  <root>
    <level value="DEBUG" />
    <appender-ref ref="Console" />
  </root>
</log4net>

逐项解说:OrderService.Dao 单独设 INFO;Appender 上挂 LevelRangeFilter(DEBUG~ERROR),levelMax=ERROR 意味着 FATAL 即使过了 Logger 级别检查也会被 Appender 拦下;%logger{1} 只输出 Logger 名最后一段。

测试程序(替换 Program.cs 的 Main 主体逻辑):

using System;
using System.IO;
using log4net;
using log4net.Config;

namespace Log4NetDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            XmlConfigurator.Configure(new FileInfo("hierarchy.config"));

            ILog order = LogManager.GetLogger("OrderService");
            ILog orderDao = LogManager.GetLogger("OrderService.Dao");
            ILog userDao = LogManager.GetLogger("UserService.Dao");

            orderDao.Debug("Dao 子 logger:SELECT * FROM t_order");
            orderDao.Error("Dao 子 logger:订单 1001 落库失败");
            userDao.Info("另一分支 UserService.Dao 的 INFO 输出");
            order.Fatal("父 logger OrderService 的 FATAL 输出");
            Console.WriteLine("---- 层次继承输出结束 ----");
        }
    }
}

实测输出:

[ERROR] Dao - Dao 子 logger:订单 1001 落库失败
[INFO ] Dao - 另一分支 UserService.Dao 的 INFO 输出
---- 层次继承输出结束 ----

逐项解说:orderDao.Debug 被 OrderService.Dao 的 INFO 级别拦截(层次继承生效);userDao 未单独配置,继承 root 的 DEBUG,INFO 通过;order.Fatal 过了 Logger 级别检查却被 LevelRangeFilter 的 levelMax=ERROR 拦截——Filter 与级别是两道独立闸门。排障插曲:模板曾写成 %logger{name},实测报 log4net:ERROR NamedPatternConverter: Precision option "name" not a decimal integer.,%logger 精度只接受数字。

5.2 RollingFileAppender 滚动压测

配置 roll.config:

<?xml version="1.0" encoding="utf-8"?>
<log4net>
  <appender name="Rolling" type="log4net.Appender.RollingFileAppender">
    <file value="logs/roll.log" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maximumFileSize value="10KB" />
    <maxSizeRollBackups value="3" />
    <staticLogFileName value="true" />
    <countDirection value="1" />
    <preserveLogFileNameExtension value="true" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
    </layout>
  </appender>
  <root>
    <level value="INFO" />
    <appender-ref ref="Rolling" />
  </root>
</log4net>

逐项解说:maximumFileSize 降到 10KB 便于压测触发滚动;countDirection=1 使用递增编号命名;preserveLogFileNameExtension=true 保留 .log 后缀(roll.1.log 而非 roll.log.1)。

压测程序(循环 300 条 INFO):

using System;
using System.IO;
using log4net;
using log4net.Config;

namespace Log4NetDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            XmlConfigurator.Configure(new FileInfo("roll.config"));
            ILog log = LogManager.GetLogger("RollDemo");
            for (int i = 1; i <= 300; i++)
            {
                log.Info("滚动压测第 " + i + " 条:订单 1001 状态更新为 PAID");
            }
            Console.WriteLine("已写入 300 条 INFO 日志");
        }
    }
}
dotnet run
ls -la bin/Debug/net8.0/logs/
head -1 bin/Debug/net8.0/logs/roll.log
head -1 bin/Debug/net8.0/logs/roll.1.log

实测输出:

已写入 300 条 INFO 日志
total 48
drwxr-xr-x 2 root root  4096 Oct 14 16:52 .
drwxr-xr-x 4 root root  4096 Oct 14 16:52 ..
-rw-r--r-- 1 root root 10292 Oct 14 16:52 roll.1.log
-rw-r--r-- 1 root root 10296 Oct 14 16:52 roll.2.log
-rw-r--r-- 1 root root 10296 Oct 14 16:52 roll.3.log
-rw-r--r-- 1 root root   208 Oct 14 16:52 roll.log
2026-10-14 16:52:55,784 [1] INFO  RollDemo - 滚动压测第 299 条:订单 1001 状态更新为 PAID
2026-10-14 16:52:55,767 [1] INFO  RollDemo - 滚动压测第 1 条:订单 1001 状态更新为 PAID

逐项解说:300 条约 30 KB 写满 10KB×3 的配额——roll.1/2/3.log 各约 10 KB(roll.1.log 首行是第 1 条,最老内容在小编号),当前 roll.log 只剩最后两三条(第 299 条起);maxSizeRollBackups=3 之外的更早文件被滚动删除。日志完整性自证:第 1 条与第 299 条均在,中间条目分片保存。

5.3 内部调试排障(log4net debug)

怀疑配置没生效时打开内部诊断:配置根元素加 debug 属性,日志输出到控制台跟踪(Trace):

<log4net debug="true">

实测回显(节选,首尾):

log4net: Configuration update mode [Merge].
log4net: Logger [root] Level string is [INFO].
log4net: Logger [root] level set to [name="INFO",value=40000].
log4net: Loading Appender [Rolling] type: [log4net.Appender.RollingFileAppender]
log4net: Setting Property [File] to String value [logs/roll.log]
log4net: Searched for existing files in [/root/dotnetdemo/Log4NetDemo/bin/Debug/net8.0/logs]
log4net: Opening file for writing [/root/dotnetdemo/Log4NetDemo/bin/Debug/net8.0/logs/roll.log] append [True]
---- 层次继承输出结束 ----
log4net: Shutdown called on Hierarchy [log4net-default-repository]

逐项解说:内部日志完整暴露了属性注入过程(Setting Property [File])、文件解析基准目录(Searched for existing files in .../bin/Debug/net8.0/logs——正是相对路径之谜的答案)与仓库关闭时机。排障完成后记得移除 debug="true",它会显著放大输出量。

六、常见问题(FAQ)

Q1:dotnet 命令找不到怎么办? A:dotnet-install.sh 装到自定义目录后需要 export PATH=/usr/local/dotnet:$PATH;要持久化就写入 /etc/profile.d/dotnet.sh 或 ~/.bashrc。验证 dotnet --version 输出 8.0.426。

Q2:dotnet add package 恢复 NuGet 失败怎么办? A:为进程导出代理变量(http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY)后重试;.NET 的 HttpClient 会读取这些环境变量。实测走代理后 5.75 秒完成恢复。

Q3:日志文件为什么出现在 bin/Debug/net8.0/logs 而不是运行目录? A:log4net 的 File 相对路径基于应用程序基目录解析。实测内部调试输出 Searched for existing files in [.../bin/Debug/net8.0/logs] 证实。生产写绝对路径即可根治。

Q4:repository.Threshold.Level 编译报 CS1061 怎么回事? A:Threshold 属性的类型是 Level 对象,它自身没有 Level 成员,文本值在 .Name 上。实测错误信息 'Level' does not contain a definition for 'Level',改成 repository.Threshold.Name 即可。

Q5:%logger{name} 为什么报 Precision option "name" not a decimal integer? A:%logger 的花括号参数是精度数字(保留几段),写单词会被当成非法精度报错。取最后一段用 %logger{1},取两段 %logger{2};%c 是 %logger 的别名。

Q6:FATAL 明明过了 root 的 DEBUG 级别,为什么没输出? A:被 Appender 上的 LevelRangeFilter(levelMax=ERROR)拦了。实测证明 Filter 在级别检查之后独立生效;删掉 Filter 或把 levelMax 提到 FATAL 即恢复。

Q7:怎么让日志不重复输出? A:重复通常来自 additivity——子 Logger 与 root 挂了相同 Appender,事件先写子再传父再写一遍。解决:只挂 root;或对需要隔离的 Logger 显式加 <additivity value="false"/>。

Q8:如何输出毫秒级时间戳? A:%date 默认格式已含毫秒(实测 2026-10-14 16:50:46,312);需要自定义时用 %date{yyyy-MM-dd HH:mm:ss.fff}。逗号分隔的毫秒来自默认模式,%date 模板串完全遵循 .NET 时间格式。

Q9:程序退出时日志会丢吗? A:同步 Appender 不丢(写完才返回);使用 BufferingForwardingAppender 等缓冲 Appender 时进程异常退出会丢缓冲区数据,正常退出会 flush。关键路径建议同步写或减小 bufferSize。

Q10:Ubuntu 上能用 EventLogAppender 吗? A:不能。EventLogAppender 依赖 Windows 事件日志服务,Linux 上初始化会失败。跨平台项目选 File/Rolling/Console/UDP 等平台无关 Appender,Windows 服务监控需求在对应平台上启用。

七、参考链接

  1. log4net NuGet 包:https://www.nuget.org/packages/log4net/
  2. log4net 配置参考(Appender/Layout/Filter 参数全表):https://logging.apache.org/log4net/release/config-reference.html
  3. log4net SDK 文档(ILoggerRepository/ILog):https://logging.apache.org/log4net/release/sdk/
  4. dotnet-install 脚本官方说明:https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script
  5. .NET 8 下载页:https://dotnet.microsoft.com/download/dotnet/8.0
  6. Apache log4net 项目主页:https://logging.apache.org/log4net/