简介:log4net-1.2.10 是 Apache 基金会维护的经典 .NET 日志记录框架,面向需要在 ASP.NET、Windows 服务、WPF 或 WinForms 项目中构建稳定日志体系的开发者。资源同时提供源码与 DLL,既可直接引用到工程中,也便于深入理解其内部实现与扩展机制。压缩包为 rar 格式,整体约 6.79MB,文件总数暂未提供明细,主要包含可引用的程序集与配套源码文件,源码可用于研读 Appender、Layout 等核心组件的设计思路。该版本支持 XML、代码或属性文件多种配置方式,涵盖 DEBUG 至 FATAL 多级别记录,并具备线程安全与良好性能,可满足调试、异常捕获、性能分析与系统监控等场景。目前已有 270 人学习关注,适合希望掌握声明式日志配置、自定义输出端与格式,并提升项目可维护性的 .NET 开发者参考使用。
1. 为什么老项目还在用 log4net-1.2.10:一次版本考古与落地判断
接手一个 .NET Framework 4.0 的遗留系统时,我在packages.config里看到log4net版本号停在1.2.10,第一反应是「这版本也太老了」。但翻完配置文件、跑完一轮日志输出后,我改主意了——这个版本能在大量老项目里活到今天,不是没道理的。log4net-1.2.10 是 Apache log4net 的一个稳定发布版,面向 .NET Framework 2.0/3.5/4.0 时代,核心能力是把日志输出从业务代码里彻底解耦:你写ILog.Info(...),具体写到文件、控制台还是数据库,由配置文件说了算。它适合谁?维护老系统的工程师、需要给 WinForm/WCF/老 ASP.NET 项目补日志的人、以及被要求「不许升级依赖」的现场开发。这篇笔记不讲 log4net 是什么科普,而是把我拆这个版本时踩过的配置、路径、线程和排错问题摊开讲,让你拿到包就能跑起来。
2. 拆开 log4net-1.2.10:程序集结构与配置加载链路
2.1 程序集里到底有什么
log4net-1.2.10 的发布包通常包含几个关键程序集:log4net.dll(核心)、按框架版本分目录的net/2.0、net/3.5、net/4.0、net/4.0-client等。选错目录是新手第一个翻车点——把net/2.0的 dll 塞进 .NET 4.0 项目,编译能过,运行时某些反射路径会出问题。我一般直接选net/4.0下的版本,除非项目明确跑在 Client Profile 上。
核心类型就几个,记住它们的关系比背 API 有用:
| 类型 | 作用 | 你会在哪用到 |
|---|---|---|
ILog | 日志接口 | 业务代码里声明字段 |
LogManager | 日志管理器 | 获取 ILog 实例 |
XmlConfigurator | 配置加载器 | 启动时读配置 |
Appender | 输出目标 | 配置文件里定义 |
Layout | 输出格式 | 配置文件里定义 |
Level | 日志级别 | 过滤和阈值 |
2.2 配置加载的三种方式和它们的坑
log4net 最容易被误解的一点:它不会自动读你的app.config里的自定义节点,除非你显式调用配置方法。常见做法有三种。
第一种,读独立配置文件:
// 在程序启动入口(Main 或 Global.asax 的 Application_Start)调用 // 参数是配置文件相对路径,注意这里是相对于工作目录,不是相对于 exe log4net.Config.XmlConfigurator.Configure( new System.IO.FileInfo("log4net.config"));第二种,读app.config/web.config里的<log4net>节点:
// 需要先在 AssemblyInfo.cs 里加这一行特性 // [assembly: log4net.Config.XmlConfigurator(Watch = true)] // 然后启动时调用无参版本 log4net.Config.XmlConfigurator.Configure();第三种,代码里直接配,适合测试:
var hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository(); var consoleAppender = new log4net.Appender.ConsoleAppender(); consoleAppender.Layout = new log4net.Layout.PatternLayout("%d [%t] %-5p %c - %m%n"); hierarchy.Root.AddAppender(consoleAppender); hierarchy.Root.Level = log4net.Core.Level.Debug; hierarchy.Configured = true;逻辑说明:第一种最灵活,配置文件可以独立于 exe 部署,改日志级别不用重编译;第二种适合 Web 项目,配置集中;第三种只建议在单元测试里用,生产环境写死配置等于给自己埋雷。参数上,Watch = true会让 log4net 监听配置文件变化并自动重载,调试期很有用,但生产环境频繁重载有性能开销,我一般关掉。
2.3 一个能直接抄的最小配置
下面这份配置我用了很多次,输出到按日期滚动的文件,同时保留控制台:
<?xml version="1.0" encoding="utf-8"?> <log4net> <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <!-- 日志文件路径,用相对路径时基准是工作目录 --> <file value="logs\\app.log" /> <appendToFile value="true" /> <!-- 按日期滚动,同时限制单个文件大小 --> <rollingStyle value="Composite" /> <datePattern value="yyyyMMdd" /> <maxSizeRollBackups value="30" /> <maximumFileSize value="10MB" /> <staticLogFileName value="true" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> </appender> <root> <level value="INFO" /> <appender-ref ref="RollingFileAppender" /> </root> </log4net>参数说明:rollingStyle设为Composite表示同时按日期和大小滚动,这是老项目里最稳的组合;maxSizeRollBackups是保留的历史文件数,设 30 意味着最多留 30 个备份,不设会无限增长把磁盘吃满;staticLogFileName为true时当前日志文件名固定,历史文件带日期后缀,方便运维按名字找当前日志。conversionPattern里的%thread在排查并发问题时是救命字段,别省。
3. 把日志接进业务代码:ILog 声明、级别控制与线程安全
3.1 ILog 字段的正确声明姿势
log4net 官方推荐每个类声明一个static readonly ILog,而不是每次LogManager.GetLogger:
public class OrderService { // 静态只读,类型名作为 logger 名,方便按类过滤 private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(OrderService)); public void Submit(int orderId) { log.InfoFormat("开始提交订单 {0}", orderId); try { // 业务逻辑 log.Debug("订单校验通过"); } catch (Exception ex) { // 异常一定要带 ex,否则堆栈全丢 log.Error("提交订单失败 " + orderId, ex); throw; } } }逻辑说明:GetLogger(typeof(OrderService))拿到的 logger 名是完整类型名,配置文件里可以用<logger name="MyApp.OrderService">单独调这个类的级别,不用动全局。InfoFormat比字符串拼接好在——当日志级别被过滤掉时,格式化参数不会执行,省性能。异常日志必须把ex作为第二个参数传进去,只写log.Error(ex.Message)等于把堆栈扔了,线上排查时你会后悔。
3.2 级别过滤和 logger 继承
log4net 的级别从低到高是DEBUG < INFO < WARN < ERROR < FATAL。root 上设的级别是全局阈值,子 logger 默认继承 root 的级别,但可以覆盖:
<root> <level value="WARN" /> <appender-ref ref="RollingFileAppender" /> </root> <!-- 只让订单模块输出 DEBUG,其他模块维持 WARN --> <logger name="MyApp.OrderService"> <level value="DEBUG" /> </logger>这里有个反直觉的点:子 logger 的 appender 是叠加的,不是覆盖的。如果你在<logger>里又加了一个 appender,这个 logger 的日志会同时写进 root 的 appender 和它自己的 appender,出现重复日志。要避免重复,要么用additivity="false",要么别在子 logger 里重复挂 appender。我见过不止一个项目因为这个问题,日志文件里每条记录出现两遍,查了半天以为是代码调了两次。
3.3 多线程下日志会不会乱
log4net 的 appender 默认是线程安全的,FileAppender内部有锁,多个线程同时写不会把行写串。但有两个边界要注意:一是PatternLayout里的%thread拿的是线程名,线程池线程名可能重复,别拿它当唯一标识;二是如果自定义 appender 没做同步,多线程下会翻车。老项目里常见的做法是给日志加一个缓冲 appender,但BufferingAppenderSystem在进程崩溃时可能丢最后一批日志,排查崩溃问题时反而误事,我一般不用。
4. 避坑排查:log4net-1.2.10 最常见的五类翻车
4.1 日志文件死活不生成
现象:代码调了log.Info,配置文件也写了,但logs目录下什么都没有,也不报错。
原因:九成是配置没加载成功。log4net 在配置失败时默认静默,不抛异常。常见触发点是XmlConfigurator.Configure()没调用,或者配置文件路径写错——注意FileInfo("log4net.config")的基准是当前工作目录,不是 exe 所在目录,在服务里工作目录可能是C:\Windows\System32。
解决:先开内部调试看 log4net 自己怎么说:
// 在 Configure 之前调用,log4net 会把内部日志打到控制台 log4net.Util.LogLog.InternalDebugging = true; log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo(configPath));打开后如果看到XmlConfigurator: configuring repository之类的输出,说明配置读到了;如果看到Could not find file,就是路径问题。生产环境记得关掉,否则控制台会被刷屏。
4.2 文件被占用,删不掉也改不了
现象:想删日志文件,提示「文件正被另一进程使用」。
原因:FileAppender默认以共享读的方式打开文件,进程活着就一直持有句柄。这是设计行为,不是 bug。
解决:要删就先停进程;如果必须热删,用RollingFileAppender的滚动机制让它自己切走,或者配置lockingModel为log4net.Appender.FileAppender+MinimalLock,代价是写入性能下降。我一般不动这个,运维删日志前先停服务是更省事的约定。
4.3 中文乱码
现象:日志文件里中文变成问号或方块。
原因:FileAppender默认编码不是 UTF-8,在部分系统区域设置下会按 ANSI 写。
解决:在 appender 里显式指定编码:
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <file value="logs\\app.log" /> <encoding value="utf-8" /> <!-- 其余配置省略 --> </appender>加上<encoding value="utf-8" />后,用记事本、VS Code、Notepad++ 打开都不会乱。注意如果历史日志已经是 ANSI 编码,新配置不会改旧文件,别拿旧文件验证。
4.4 日志把磁盘写满
现象:服务器磁盘告警,一查是日志目录几十个 G。
原因:maxSizeRollBackups没设,或者设了但rollingStyle是Size而maximumFileSize太大,单个文件涨到几个 G 才切。
解决:用第 2 章那份Composite配置,maxSizeRollBackups给一个合理值(我一般 20 到 50),maximumFileSize控制在 10MB 到 50MB。另外注意RollingFileAppender的备份清理只在滚动发生时触发,如果日志量很小一直不滚动,旧文件也不会被清,这种情况要靠外部脚本定期清理。
4.5 升级或换机器后配置失效
现象:本地跑得好好的,部署到服务器就不写日志。
原因:配置文件没随发布包带过去,或者app.config里的<log4net>节点在发布时被合并工具弄丢了。老项目的发布脚本经常只拷 dll 不拷 config。
解决:把log4net.config的「复制到输出目录」属性设为「始终复制」,并在部署检查清单里加一条「确认日志配置文件存在且可读」。另外 IIS 下要注意应用程序池账户对日志目录有没有写权限,没权限时 log4net 同样是静默失败,配合 4.1 的内部调试能快速定位。
5. 进阶技巧:用代码动态调级别与验证配置是否生效
5.1 运行时改级别,不用重启
线上出问题时,把级别从 INFO 临时调到 DEBUG,抓完日志再调回去,比重启服务优雅得多。log4net 支持运行时改:
// 拿到指定 logger 并改它的级别 var logger = log4net.LogManager.GetLogger("MyApp.OrderService") as log4net.Repository.Hierarchy.Logger; if (logger != null) { // 临时打开 DEBUG logger.Level = log4net.Core.Level.Debug; // 排查完记得恢复,或者干脆恢复为 null 让它继承 root // logger.Level = null; }逻辑说明:LogManager.GetLogger返回的是ILog,要改级别得转成Repository.Hierarchy.Logger。把Level设为null表示继承父级(通常是 root)的级别,这是恢复默认最干净的方式。这个技巧在只能远程连的生产环境里特别有用,但要注意——如果 root 级别本身是 WARN,你把子 logger 设成 DEBUG 后,DEBUG 日志会输出,但 root 的 appender 阈值如果也卡着,可能还是看不到,得连 appender 的Threshold一起看。
5.2 验证配置到底加载了哪些 appender
配置写多了以后,你未必清楚最终生效的是哪套。我习惯在启动后打一段自检:
var repository = log4net.LogManager.GetRepository(); var appenders = repository.GetAppenders(); foreach (var appender in appenders) { // 输出每个 appender 的名字和类型,确认没有多余或缺失 System.Diagnostics.Debug.WriteLine( "Appender: " + appender.Name + " / " + appender.GetType().Name); }如果这里打印出的 appender 数量和你配置文件里写的不一致,说明有配置被覆盖或没读到。这个方法比盯着 XML 猜要快得多,尤其是在多层配置合并的项目里。
5.3 一个我坚持了很多年的习惯
从那以后我每次给老项目接 log4net,都强制走一遍「内部调试开一次 → 确认 appender 列表 → 写一条测试日志 → 检查文件编码和滚动」这四步,不管项目多小。因为 log4net 的静默失败太坑,等线上出问题再回头查配置,成本比启动时多花五分钟高得多。这份 log4net-1.2.10 的资源包,程序集、配置示例和上面这些排查点都能直接对上,拿到后建议先在本地空项目里跑通最小配置,再往业务里接。希望帮到你。
本文还有配套的精品资源,点击获取