
先说我最近又折腾这个配置的原因。上周负责的一个.NET Core定时任务服务跑了不到一周磁盘就报警了。上去一查日志目录里躺着一个快20GB的txt文件才反应过来当初图省事直接用了Serilog的默认文件输出从来没有设置过按指定大小生成日志的切分策略。后来把所有服务统一改成代码方式配置Serilog才把这些坑一次填平。这篇东西就是把我踩过的坑和最终采用的方案整理出来。适用于所有用.NET Core Serilog做日志落盘的场景无论你是Web API、Worker Service还是控制台程序只要你想在C#代码里直接控制日志文件按大小滚动、不依赖appsettings.json这篇文章可以直接照着抄。先说一个很多新手容易忽略的事实Serilog虽然默认用配置文件很顺手但代码方式配置其实更可控。尤其是你需要根据环境变量动态调整日志级别、把日志组件封装成通用库给多个项目共用、或者做统一的微服务日志规范时代码配置比JSON配置文件可靠得多。JSON文件容易在不同环境部署时漏同步而代码写得对所有环境行为一致。而且编译期就能检查配置写错没写错比运行时靠读字符串更能提前暴露问题。1. 为什么我把日志配置从appsettings.json迁到了代码里这个动机先讲清楚不然有人会觉得“配置文件明明也能配折腾代码干嘛”。最直接的触发点是我那个20GB日志文件。回想起来最初版本只写了这一句Log.Logger new LoggerConfiguration() .WriteTo.File(logs/task-.log, rollingInterval: RollingInterval.Day) .CreateLogger();看起来挺对按天滚动每天一个新文件。但问题是这个服务一天要写几百MB日志到了晚上一个文件就1GB多而Serilog默认的单个文件没有做任何大小限制于是文件会一直涨到把磁盘写满。配置文件方案也没解决因为我当时压根没意识到需要同时开启大小滚动。后来我对比了两种方式的差异整理成一张表对比项appsettings.json配置代码配置编译期检查无配置错误运行时才暴露有参数名写错编译不通过环境差异化每个环境要维护不同JSON代码里通过环境变量分支控制复用性每次复制JSON片段封装成扩展方法一个调用搞定动态调整级别要改文件并重启代码里可接LoggingLevelSwitch动态改默认值陷阱配置项默认值容易漏配参数显式传值含义清晰从前端读到这你可能觉得我在小题大做但对维护十几个服务的团队来说JSON文件散落各处改起来是真的累。封装成NuGet包或者公共的静态类之后所有服务统一调一个方法参数集中管理基本不会出现生产环境和测试环境配置不一致的情况。这也是我后来在项目里坚持的写法日志不是某个服务的私有需求而是基础能力基础能力就该用代码约束起来。2. 按大小滚动日志的真实机制fileSizeLimitBytes与rollOnFileSizeLimit如果不理解Serilog内部对文件大小的判断逻辑配出来的东西很可能“看起来生效了实际上没生效”。Serilog.Sinks.File里有两个参数负责大小滚动一个是fileSizeLimitBytes一个是rollOnFileSizeLimit。很多人只设置了前者发现日志文件还是会无限膨胀就是因为没有理解这两个参数的联动关系。先说fileSizeLimitBytes。它是单个日志文件允许写入的最大字节数。注意官方默认值其实是1GB1 * 1024 * 1024 * 1024字节。也就是说如果你不显式传这个值文件最多写到1GB也不会再继续。但问题是到1GB之后Serilog并不会自动开始一个新文件而是干脆什么都不写了——除非你同时把rollOnFileSizeLimit设为true。rollOnFileSizeLimit直接翻译就是“到达大小限制时滚动”。这个开关默认是false只有把它显式设为trueSerilog才会在单个文件达到fileSizeLimitBytes上限后关闭当前文件创建下一个新文件继续写。简单总结触发链路写入一条日志前Serilog检查当前文件大小加本次消息长度是否超过fileSizeLimitBytes。没超继续写当前文件。超了且rollOnFileSizeLimittrue关闭当前文件流按文件名模板生成带序号的新文件日志写到新文件里。超了但rollOnFileSizeLimitfalse日志丢失什么都不发生。另一个相关参数是rollingInterval。它决定文件名按什么时间周期滚动默认是RollingInterval.Infinite也就是根本不按时间滚动。很多人以为Serilog默认按天滚动其实代码API里如果你没显式传它就是无限期不滚动。我见过好几个项目因为这个默认值日志全写在同一个文件里。所以配置时我建议显式写rollingInterval: RollingInterval.Day或者RollingInterval.Hour哪怕你的核心需求是按大小滚动时间维度也最好带上后面排查问题能更快定位到某一天的日志。时间滚动和大小滚动两者不是互斥的可以同时生效。比如文件模板写logs/app-.logrollingInterval设为Day加上rollOnFileSizeLimit和fileSizeLimitBytes之后同一个自然日内如果文件达到大小上限Serilog会在文件名后追加序号生成类似app-20250313_001.log这样的文件。等到第二天文件名又变回app-20250314.log。理解这个机制之后配置的核心点就清晰了单文件上限给一个合理值切分开关打开时间维度按你的日志量决定。日志量大的服务按小时滚动更合适否则一天一个文件夹里堆出几十个分片查看的时候一样痛苦。3. 可直接复制的完整代码配置与关键参数解释下面这段是我在实际项目里用的日志配置封装核心逻辑就是按指定大小生成日志文件。你先安装包再直接抄这段。安装NuGet包dotnet add package Serilog dotnet add package Serilog.Sinks.File dotnet add package Serilog.Extensions.Logging dotnet add package Serilog.Settings.Configuration如果是ASP.NET Core项目可以直接装Serilog.AspNetCore它会把上面几个包一起带进来。完整配置代码using Serilog; using Serilog.Events; using Serilog.Formatting.Compact; public static class LoggingSetup { public static LoggerConfiguration CreateConfiguration() { var config new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override(Microsoft, LogEventLevel.Warning) .MinimumLevel.Override(System, LogEventLevel.Warning) .Enrich.FromLogContext() .Enrich.WithProperty(Application, MyService) .WriteTo.File( path: logs/app-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 50 * 1024 * 1024, // 单文件50MB retainedFileCountLimit: 30, // 最多保留30个文件 outputTemplate: {Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}, shared: false, flushToDiskInterval: TimeSpan.FromSeconds(1) ); return config; } }Program.cs里使用控制台程序先试这个using Serilog; Log.Logger LoggingSetup.CreateConfiguration().CreateLogger(); try { Log.Information(服务启动); // 业务代码... } catch (Exception ex) { Log.Fatal(ex, 服务异常终止); } finally { Log.CloseAndFlush(); }如果你的项目是ASP.NET Core可以这么接进Host里var builder WebApplication.CreateBuilder(args); builder.Host.UseSerilog((context, services, configuration) { configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.File( path: logs/app-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 50 * 1024 * 1024, retainedFileCountLimit: 30, shared: true ); }); var app builder.Build(); app.Run();每个参数我实际测下来给一个说明方便你按需调整参数我的推荐值作用注意点pathlogs/app-.log文件路径模板文件名里必须带-不带横线的话滚动时会覆盖同一个文件rollingIntervalRollingInterval.Day按天生成一组文件日志量大的服务可以考虑HourrollOnFileSizeLimittrue达到大小上限后自动创建新文件不设true的话大小限制约等于让日志丢失fileSizeLimitBytes50MB或100MB单文件大小上限太小会导致文件碎片多太大会造成排查困难retainedFileCountLimit30最多保留多少份历史文件默认31份日志量大的话需要显式调大或设nullsharedfalse多进程是否共享写入同一文件Web应用多实例时建议开trueflushToDiskInterval1秒定时把缓冲刷到磁盘不设的话默认每条日志都刷性能开销较大实测观察我的服务每天日志量约800MB原来一天一个文件涨到1GB改成单文件100MB滚动后一天大概产生9~10个分片文件每个文件名按日期序号递增。查看某一天的日志只需要按文件名过滤比在巨型文件里tail和grep快得多而且定位问题时的心情也好多了。4. 实际调试时踩过的三个坑文件不切分、文件锁、日志丢失写代码五分钟改Bug两小时。把我在实战中遇到的坑完整列出来省得你再走一遍。4.1 只设fileSizeLimitBytes日志文件还是无限增长这是我第一个踩的坑也是最容易踩的。现象文件一直写我设的fileSizeLimitBytes: 104857600100MB毫无作用文件超过100MB还在继续写。排查过程先确认是不是没重启进程。查了已重启。再翻NuGet包版本确认Serilog.Sinks.File版本够新。最后仔细读了一遍API文档发现rollOnFileSizeLimit参数标注的说明是Whether to roll the file to a new file when the maximum file size is reached默认false。找到问题了。我根本没传rollOnFileSizeLimit: trueSerilog只会默默不写入新内容而不会自动切文件。所谓“按指定大小生成日志方式”正确且完整的人话翻译是先打开rollOnFileSizeLimit开关再指定fileSizeLimitBytes大小缺少任何一个都不能实现切分。加上之后我用下面的脚本快速验证// 快速验证生成足够多的日志观察文件切分 for (int i 0; i 100000; i) { Log.Information(测试滚动日志内容 {Index}, i); if (i % 100 0) { Thread.Sleep(10); } }设置fileSizeLimitBytes: 1 * 1024 * 10241MB跑完能看到目录下多出好几个带序号的文件确认生效。4.2 多进程同时写同一个日志文件引发的日志丢失第二个坑出现在我把服务部署成多实例之后。现象某些日志凭空消失且Windows事件查看器里能看到程序的异常。查下来是多个进程实例同时尝试写同一个app-20250313.log文件文件句柄冲突部分写入被丢弃。这个问题的本质是Serilog默认对文件是独占打开的。多进程共享同一个日志文件需要显式打开shared: true参数。这一个参数允许不同进程共享同一个文件写句柄代价是性能稍有下降但对大多数业务系统来说完全可接受。解决方案很简单.WriteTo.File( path: logs/app-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 50 * 1024 * 1024, shared: true )如果你不想多进程共用一个日志文件另一个方案是让每个实例生成独立文件名在路径里带上实例编号或环境名比如logs/app-{Environment}-.log。这个方案隔离性更好但排查时要先定位是哪个实例产生了日志。我自己的习惯是内部管理系统shared: true就够了面向用户的高并发API更建议每个实例独立文件。4.3 文件被占用导致历史日志清理失败、磁盘持续膨胀第三个坑和Windows环境强相关遇到的人不多但遇到就很痛。现象程序在Windows上跑我设置了retainedFileCountLimit: 30但日志文件并没有按预期删除磁盘占用持续增长最后发现垃圾文件全堆在目录里。排查后发现程序某个线程还在持有日志文件的句柄。Serilog虽然写了“删除历史文件”的逻辑但Windows上如果目标文件被其他进程或服务内部的另一个logger实例占用删除会直接失败然后Serilog会放弃这次清理。解决思路确保全局只有一个Logger实例不要每次new一个Logger写同一路径。应用程序退出前调用Log.CloseAndFlush()显式释放句柄。如果服务里有自己维护的FileStream也在写日志别图省事混在一起统一走Serilog。另外一个被很多人忽略的点retainedFileCountLimit默认值是31意思是最多保留31个文件不是保留31天。按天滚动时31份差不多一个月但如果你按小时滚动还开着大小切分31份可能只覆盖两三天。那时候你需要显式调大或者干脆设置retainedFileCountLimit: null交给外部定期清理任务去处理避免Serilog在启动和运行时频繁做文件枚举和删除操作。5. 光“按大小切分”不够这几项配置建议一起补上到了这步你已经能做到按指定大小自动切分日志文件了。但实际生产中光有切分还是不够的我慢慢补充几个提升体验的配置项。5.1 buffered提升写入吞吐默认情况下Serilog每写一条日志就同步刷一次磁盘这也是慢的一环。高并发场景下日志写入会对业务线程产生明显的性能损耗。可以开启buffered: true让日志进入缓冲区攒够一批再写盘。代价是进程崩溃时可能丢失缓冲区里未落盘的数据所以核心交易类日志慎开对性能和可靠性的取舍要清楚。5.2 动态调整日志级别LoggingLevelSwitch运维同学最烦的应该就是“日志级别不够查不到问题调低了又刷屏”。代码里加一个LoggingLevelSwitch就可以做到不重启进程动态调整级别var levelSwitch new LoggingLevelSwitch(LogEventLevel.Information); var config new LoggerConfiguration() .MinimumLevel.ControlledBy(levelSwitch) .WriteTo.File( path: logs/app-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 50 * 1024 * 1024 ) .CreateLogger(); // 运行中动态调成Debug levelSwitch.MinimumLevel LogEventLevel.Debug;如果你把levelSwitch对象注册到依赖注入容器甚至可以做一个管理接口在线切换所有服务的日志级别。这一招在微服务环境里真的很实用。5.3 结构化日志给日志加上可检索的字段Serilog的看家本领是结构化日志。如果只用outputTemplate输出文本完全浪费了它的潜力。我建议至少保留Enrich.FromLogContext()和Enrich.WithProperty(Application, MyService)这样文件里虽然还是纯文本但每个日志条目会带固定的上下文字段后续导出到ES等平台时能直接做字段映射。如果你是全链路日志系统可以尝试CompactJsonFormatter.WriteTo.File( new CompactJsonFormatter(), logs/app-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 50 * 1024 * 1024 )JSON格式占的空间更大换来的是机器可解析。两种格式我建议至少搞定一种不要只停留在人肉grep文本阶段。5.4 文件名带上环境名区分测试和生产这块属于部署层面的细节了。多个环境共用同一个日志采集目录时文件名里最好带上环境标识var environment Environment.GetEnvironmentVariable(ASPNETCORE_ENVIRONMENT) ?? Production; .WriteTo.File( path: $logs/app-{environment}-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 50 * 1024 * 1024 )说白了按指定大小生成日志的核心解决了“单个文件无限膨胀”问题但一个完整的日志治理方案还要考虑文件数量上限、写入性能、检索效率和多环境隔离。把它们一起配好之后日志系统才算达到生产可用级别。6. 补充一个脚本快速测试日志切分是否生效如果不想等生产环境跑出几百MB日志才确认配置有没有问题可以写一个临时测试接口或者控制台循环。这是我测试时用的脚本也可以直接在UnitTest里跑。public void SizeRollingShouldWork() { Log.Logger new LoggerConfiguration() .WriteTo.File( path: test-logs/test-.log, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 1024 * 1024, // 1MB测试用小一点 retainedFileCountLimit: 5 ) .CreateLogger(); // 连续写随机内容观察目录下的文件分片 var payload new string(X, 1024); // 1KB左右每条 for (var i 0; i 5000; i) { Log.Information({Payload}{Index}, payload, i); } Log.CloseAndFlush(); var files Directory.GetFiles(test-logs, *.log); Assert.True(files.Length 1, 期待一份以上日志文件实际只有一份说明滚动未生效); }跑完以后进test-logs目录看一眼你会发现1MB大小限制下很快就切分成多个文件了。测试通过再把这个配置平移进正式项目里基本不会有意外。最后说一个我在生产环境里的体会日志文件的大小切分不是越细越好。文件切得太碎比如每个几百KB收集和统计时会很麻烦很多日志分析工具拉取文件本身的开销比分析内容还大。单文件大小我通常建议50MB到200MB之间既方便工具处理又能保证切换时不会丢太多上下文。真正该关注的是磁盘占用的总量控制而不是文件个数本身。这一点你和运维同学多沟通几次就会有体感了。