Apache log4net 3.5.0 集成使用实战详解
基于Ubuntu 22.04实测log4net 3.5.0从dotnet环境搭建、NuGet引入到控制台与滚动文件示例的完整实战。
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 服务监控需求在对应平台上启用。
七、参考链接
- log4net NuGet 包:https://www.nuget.org/packages/log4net/
- log4net 配置参考(Appender/Layout/Filter 参数全表):https://logging.apache.org/log4net/release/config-reference.html
- log4net SDK 文档(ILoggerRepository/ILog):https://logging.apache.org/log4net/release/sdk/
- dotnet-install 脚本官方说明:https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script
- .NET 8 下载页:https://dotnet.microsoft.com/download/dotnet/8.0
- Apache log4net 项目主页:https://logging.apache.org/log4net/