Unity Linux游戏开发:集成Serilog实现结构化日志记录与生产环境运维
1. 项目概述为什么Unity Linux游戏开发需要Serilog如果你在Unity里做过Linux平台的游戏开发尤其是服务器端或者需要长期运行的后台服务肯定对Unity默认的日志系统又爱又恨。Debug.Log用起来是方便但日志一多就成了灾难——所有信息都混在一起像一锅乱炖想从中快速定位一个特定玩家的行为、分析一次崩溃的原因或者只是简单地按错误级别过滤一下都变得异常困难。更别提想把日志持久化到文件或者发送到像Elasticsearch、Seq这样的日志服务器进行分析了。在Linux生产环境下这种原始的日志输出方式几乎无法进行有效的运维监控和问题诊断。这就是结构化日志记录Structured Logging要解决的问题。它不仅仅是输出一段文本而是将日志信息以键值对属性的形式记录下来。比如一条日志不仅包含“玩家攻击了怪物”这个消息还会附带PlayerId: 1001,MonsterId: 2005,Damage: 150,Timestamp: 2023-10-27T10:30:00Z等一系列结构化的属性。这样后续无论是用简单的grep命令还是用专业的日志分析工具都能进行极其高效和精准的查询与分析。在.NET生态中Serilog是结构化日志记录事实上的标准。它设计优雅扩展性极强性能也非常出色。将Serilog引入Unity的C#脚本开发中意味着我们能将现代后端开发中成熟的日志实践无缝迁移到游戏客户端或服务器逻辑中。特别是在Linux环境下我们可以轻松配置Serilog将日志写入到系统日志Syslog、JSON文件方便用jq解析或者通过网络发送到中央日志聚合服务这对于部署在云服务器上的游戏服务器、独立游戏的Linux版本后台组件来说是提升可观测性和维护性的关键一步。接下来的内容我将以一个实际的Unity项目为背景详细拆解如何在Linux目标平台下集成并高效使用Serilog。我会涵盖从基础集成、关键配置、性能优化到生产环境最佳实践的完整路径并分享许多在官方文档里找不到的实操细节和踩坑经验。2. 核心思路与方案选型为什么是Serilog以及如何与Unity共处在Unity中使用第三方.NET库尤其是像Serilog这样功能丰富的库首先需要解决的是兼容性问题。Unity使用的并非完整版的.NET Framework或.NET Core而是基于Mono或较新版本基于.NET Standard/.NET Core的定制化运行时。这带来了几个核心考量点。2.1 选择Serilog的核心理由首先我们为什么坚定地选择Serilog而不是其他日志库如NLog或Microsoft.Extensions.Logging一流的结构化日志支持这是Serilog的立身之本。其日志模板语法Message Templates非常直观例如Log.Information(“Player {PlayerId} attacked {MonsterId} for {Damage} damage”, playerId, monsterId, damage)。Serilog会智能地将参数与模板中的占位符绑定生成结构化的日志事件而不仅仅是格式化的字符串。这对于后续的查询如查找所有PlayerId为1001的日志至关重要。强大的接收器Sink生态系统Serilog通过“接收器”来输出日志。其社区提供了海量的接收器几乎可以写入任何你能想到的目标控制台、文件文本/JSON、数据库、Elasticsearch、Seq、Syslog、HTTP端点等等。对于Linux环境Serilog.Sinks.File支持JSON格式、Serilog.Sinks.Syslog或Serilog.Sinks.Seq都是极其相关的选择。卓越的性能Serilog在设计上充分考虑了性能。其结构化日志捕获在大多数情况下是零分配的当使用常量消息模板时这对游戏这种对性能敏感的应用来说是个巨大优势。频繁的日志记录不会导致严重的GC垃圾回收压力。灵活的配置既支持流畅的代码配置LoggerConfiguration也支持通过JSON或appsettings.json进行配置非常灵活。2.2 Unity集成方案决策在Unity中集成Serilog主要有两种路径方案A通过Unity的包管理器UPM或直接导入DLL这是最直接的方式。Serilog的主要包如Serilog,Serilog.Sinks.Console,Serilog.Sinks.File都以.NET Standard 2.0为目标这与Unity较新版本2019.4尤其是使用.NET Standard 2.1或.NET 4.x/5/6/7/8等效框架时是兼容的。操作你可以从NuGet下载对应的.nupkg文件解压后将其中的lib/netstandard2.0/下的DLL文件放入Unity项目的Assets/Plugins文件夹。或者对于支持UPM的包可以尝试通过Git URL添加。优点直接能使用最新版本的Serilog。挑战需要手动管理依赖Serilog自身依赖较少但某些接收器可能有额外依赖。在构建尤其是IL2CPP构建时可能会遇到链接器Linker剥离未使用代码的问题导致运行时找不到类型或方法。方案B使用为Unity适配的库社区有一些项目将Serilog或其概念移植到Unity但可能版本较旧或功能有裁剪。对于追求稳定和深度集成的生产项目我通常不首选此方案因为它可能无法跟上Serilog主线的更新和生态。我的选择与建议对于大多数项目尤其是目标平台包含Linux包括通过WSL2进行开发的情况我推荐方案A。它提供了最大的灵活性和最强的功能。我们需要解决的主要是构建时的链接器问题这可以通过链接器配置文件link.xml或直接确保代码路径被引用到来解决。2.3 针对Linux环境的接收器选型在Linux服务器或桌面环境下日志的去向需要特别设计开发与调试ConsoleSink彩色输出和FileSink写入项目目录下的文本或JSON文件足矣。生产环境JSON文件使用Serilog.Sinks.File并配置JSON格式化器如Serilog.Formatting.Compact或Serilog.Formatting.Json.JsonFormatter。产生的.json文件可以被jq、Logstash或任何JSON解析工具轻松处理便于编写自动化监控脚本。系统日志Syslog使用Serilog.Sinks.Syslog或Serilog.Sinks.SyslogMessages。这允许你的游戏服务将日志发送到Linux系统的Syslog守护进程如rsyslog, syslog-ng从而集成到系统级的日志管理流程中。网络日志服务使用Serilog.Sinks.Seq发送到Seq服务器、Serilog.Sinks.Elasticsearch发送到ELK栈或Serilog.Sinks.Http。这是集中化管理多台游戏服务器日志的最佳实践。在本指南中我们将以实现一个兼顾开发控制台文件和生产JSON文件的配置为例并会特别说明Linux下的路径处理和权限问题。3. 集成Serilog到Unity项目的详细步骤这里我们假设你使用的是Unity 2021.3 LTS或更新版本并已将“API Compatibility Level”设置为.NET Standard 2.1或.NET Framework确保兼容性更好。我们将采用手动导入DLL的方式因为它最可控。3.1 获取并导入Serilog库下载NuGet包访问 nuget.org 搜索并下载以下包的最新稳定版本以2.x版本为例Serilog(核心库)Serilog.Sinks.Console(控制台输出)Serilog.Sinks.File(文件输出)Serilog.Formatting.Compact(可选用于紧凑的JSON格式)Serilog.Settings.AppSettings(可选用于从JSON配置但Unity中可能需要额外处理)提取DLL下载的.nupkg文件本质上是zip包。将其重命名为.zip并解压。我们需要的DLL位于lib/netstandard2.0/目录下。导入Unity在你的Unity项目Assets文件夹下创建Plugins/Serilog目录。将解压出的核心DLL如Serilog.dll复制到Assets/Plugins/Serilog。同样将Serilog.Sinks.Console.dll和Serilog.Sinks.File.dll复制过来。如果你需要Serilog.Formatting.Compact也将其DLL复制过来。关键步骤选中这些导入的DLL文件在Unity Inspector面板中确保“Platform”设置正确。通常取消勾选“Any Platform”然后单独勾选“Editor”、“Standalone”包括Linux子选项和“Server”如果你的项目类型包含。确保为“WSAPlayer”、“Android”、“iOS”等不相关的平台取消勾选以避免不必要的构建大小和兼容性问题。3.2 创建全局日志管理器为了避免在每个类中重复配置Logger并确保应用程序生命周期内日志的一致性我们创建一个单例或静态类来管理Serilog的ILogger实例。在Assets/Scripts/Core下创建一个LogManager.csusing Serilog; using Serilog.Core; using Serilog.Events; using UnityEngine; namespace YourGame.Core { public static class LogManager { private static ILogger _logger; public static ILogger Logger { get { if (_logger null) { // 这是一个安全的默认配置防止在未初始化时访问 _logger new LoggerConfiguration().CreateLogger(); } return _logger; } private set _logger value; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] public static void Initialize() { // 构建日志配置 var logConfig new LoggerConfiguration() .MinimumLevel.Debug() // 设置最低日志级别 .Enrich.FromLogContext() // 允许动态添加上下文属性 .Enrich.WithProperty(Application, Application.productName) .Enrich.WithProperty(Version, Application.version) .Enrich.WithProperty(UnityVersion, Application.unityVersion); // 根据运行环境添加不同的Sink #if UNITY_EDITOR // 在编辑器下使用彩色控制台输出便于调试 logConfig.WriteTo.Console( outputTemplate: “[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}”, theme: Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code); #else // 在非编辑器环境如Linux独立运行时写入到文件 // 重点处理Linux下的路径 string logDirectory; string logFilePath; if (Application.isEditor) { logDirectory Application.dataPath; } else { // 在Linux独立应用中持久化数据路径更合适 logDirectory Application.persistentDataPath; } // 确保日志目录存在 System.IO.Directory.CreateDirectory(logDirectory); // 构建日志文件路径。使用日期滚动并保存为JSON格式。 logFilePath System.IO.Path.Combine(logDirectory, “logs”, “game-.json”); // 写入到JSON文件使用Compact格式节省空间且易于解析 logConfig.WriteTo.File( path: logFilePath, rollingInterval: RollingInterval.Day, // 按天滚动 retainedFileCountLimit: 7, // 保留最近7天的日志 formatter: new Serilog.Formatting.Compact.CompactJsonFormatter(), // 输出为JSON shared: true // 允许多进程写入同一个文件谨慎使用 ); // 同时也可以输出到控制台如果Linux运行时支持 logConfig.WriteTo.Console( outputTemplate: “[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}”); #endif // 创建Logger实例 Logger logConfig.CreateLogger(); // 可选将Serilog设置为Log类的基础设施这样任何使用Microsoft.Extensions.Logging的库也能受益 // Log.Logger Logger; Logger.Information(“LogManager initialized. Platform: {Platform}, DataPath: {DataPath}”, Application.platform, Application.persistentDataPath); } // 提供一个方法用于在应用退出时安全地关闭日志 public static void CloseAndFlush() { (_logger as Serilog.Core.Logger)?.Dispose(); _logger null; } } }关键点解析与注意事项RuntimeInitializeOnLoadMethod: 这个属性确保Initialize方法在游戏场景加载前自动执行非常适合进行全局初始化。路径处理这是Linux集成的关键。Application.persistentDataPath在Linux独立应用中通常指向~/.config/unity3d/CompanyName/ProductName遵循XDG规范或类似位置这是一个用户可写且相对标准的路径。避免使用Application.dataPath因为在构建后它可能指向只读位置。日志滚动与保留RollingInterval.Day和retainedFileCountLimit: 7是生产环境的良好实践防止日志文件无限膨胀占满磁盘。在Linux服务器上你还可以结合logrotate工具进行更系统的日志管理。JSON格式使用CompactJsonFormatter输出的日志每行是一个完整的JSON对象可以直接用tail -f game-20231027.json | jq .这样的命令实时查看和过滤非常强大。共享文件写入shared: true参数允许多个进程写入同一个日志文件这在某些多进程架构的服务器上可能有用但要注意潜在的写入冲突。如果只有一个进程可以设为false。3.3 在游戏代码中使用结构化日志现在你可以在任何游戏脚本中使用LogManager.Logger来记录结构化的日志了。using UnityEngine; using YourGame.Core; // 你的LogManager所在的命名空间 using Serilog; public class PlayerCombat : MonoBehaviour { public int playerId 1001; private float _attackCooldown 1.0f; private float _lastAttackTime; void Update() { if (Input.GetKeyDown(KeyCode.Space) Time.time _lastAttackTime _attackCooldown) { AttackMonster(2005, 150); } } void AttackMonster(int monsterId, int baseDamage) { _lastAttackTime Time.time; // 计算伤害引入一些随机性 int actualDamage Mathf.RoundToInt(baseDamage * Random.Range(0.9f, 1.1f)); bool isCritical Random.value 0.1f; if (isCritical) actualDamage * 2; // 使用Serilog记录结构化日志 // 注意消息模板中的占位符名称 {PlayerId}, {MonsterId} 等会成为JSON属性名 LogManager.Logger.Information( “Player {PlayerId} attacked monster {MonsterId}. Damage: {Damage}, IsCritical: {IsCritical}, Cooldown: {Cooldown:F2}s”, playerId, monsterId, actualDamage, isCritical, _attackCooldown); // 记录错误或警告示例 if (actualDamage 0) // 假设某种情况导致伤害异常 { LogManager.Logger.Warning( “Calculated non-positive damage for player {PlayerId} vs monster {MonsterId}. BaseDamage: {BaseDamage}, Actual: {ActualDamage}”, playerId, monsterId, baseDamage, actualDamage); } // 你也可以使用LogContext动态添加上下文属性这在处理异步操作或请求链时非常有用 using (Serilog.Context.LogContext.PushProperty(“CombatSessionId”, System.Guid.NewGuid())) using (Serilog.Context.LogContext.PushProperty(“FrameCount”, Time.frameCount)) { LogManager.Logger.Debug(“Combat details within context.”); } } void OnDestroy() { // 当组件或对象销毁时记录一条信息 LogManager.Logger.Information(“PlayerCombat component destroyed for PlayerId: {PlayerId}”, playerId); } }输出示例JSON格式 当上述代码运行时在日志文件中你会看到类似这样的一行经过美化{ “t”: “2023-10-27T10:30:00.123456Z”, “l”: “Information”, “mt”: “Player {PlayerId} attacked monster {MonsterId}. Damage: {Damage}, IsCritical: {IsCritical}, Cooldown: {Cooldown:F2}s”, “PlayerId”: 1001, “MonsterId”: 2005, “Damage”: 165, “IsCritical”: true, “Cooldown”: 1.0, “Application”: “MyUnityGame”, “Version”: “1.0.0”, “UnityVersion”: “2021.3.15f1” }你可以看到所有参数都成为了结构化的属性并且自动附带了我们在LogManager中全局注入的属性Application,Version等。4. 针对Linux生产环境的进阶配置与优化在Linux服务器上部署游戏服务时日志配置需要更加健壮和自动化。4.1 使用JSON配置文件动态配置硬编码配置在开发时方便但在生产环境中我们可能希望不重新编译程序就能修改日志级别、文件路径等。我们可以利用Serilog.Settings.Configuration包但从Unity加载appsettings.json需要一些技巧。一个更Unity-friendly的方式是使用ScriptableObject或自定义文本文件。这里展示一个使用简单JSON文件的方法创建配置文件在Assets/Resources文件夹下创建logsettings.jsonResources文件夹便于运行时加载。{ “Serilog”: { “Using”: [“Serilog.Sinks.Console”, “Serilog.Sinks.File”], “MinimumLevel”: { “Default”: “Information”, “Override”: { “Microsoft”: “Warning”, “System”: “Warning”, “YourGame.AI”: “Debug” // 为你自己的AI模块设置更详细的日志级别 } }, “WriteTo”: [ { “Name”: “Console”, “Args”: { “outputTemplate”: “[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}” } }, { “Name”: “File”, “Args”: { “path”: “./logs/game-.json”, “rollingInterval”: “Day”, “retainedFileCountLimit”: 7, “formatter”: “Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact” } } ], “Enrich”: [“FromLogContext”, “WithMachineName”, “WithThreadId”], “Properties”: { “Application”: “MyGameServer”, “Environment”: “Production” } } }修改LogManager以读取配置using System.IO; using Newtonsoft.Json.Linq; // 需要导入Newtonsoft.JsonUnity已内置 // ... 其他using public static void InitializeFromConfig() { TextAsset configFile Resources.LoadTextAsset(“logsettings”); if (configFile null) { Debug.LogError(“Log configuration file not found in Resources.”); Initialize(); // 回退到默认配置 return; } var configJson JObject.Parse(configFile.text); var serilogSection configJson[“Serilog”]; // 注意这里需要手动解析配置并应用到LoggerConfiguration。 // 为了简化我们可以只读取几个关键值或者使用一个轻量级的解析器。 // 更完整的实现可以使用 Serilog.Settings.Configuration但需要解决程序集加载问题。 // 以下是一个简化示例仅动态设置最低日志级别和文件路径 string minLevel serilogSection?[“MinimumLevel”]?[“Default”]?.Valuestring() ?? “Information”; string filePath null; var writeToArray serilogSection?[“WriteTo”] as JArray; if (writeToArray ! null) { foreach (var sink in writeToArray) { if (sink[“Name”]?.Valuestring() “File”) { filePath sink[“Args”]?[“path”]?.Valuestring(); break; } } } var logConfig new LoggerConfiguration(); switch (minLevel) { case “Verbose”: logConfig.MinimumLevel.Verbose(); break; case “Debug”: logConfig.MinimumLevel.Debug(); break; case “Information”: logConfig.MinimumLevel.Information(); break; case “Warning”: logConfig.MinimumLevel.Warning(); break; case “Error”: logConfig.MinimumLevel.Error(); break; case “Fatal”: logConfig.MinimumLevel.Fatal(); break; } logConfig.Enrich.FromLogContext() .Enrich.WithProperty(“Application”, “MyGame”); // 动态应用文件路径如果配置了且不在编辑器下#if !UNITY_EDITOR if (!string.IsNullOrEmpty(filePath)) { // 处理相对路径将其转换为基于持久化数据路径的绝对路径 string absolutePath Path.Combine(Application.persistentDataPath, filePath); string logDir Path.GetDirectoryName(absolutePath); Directory.CreateDirectory(logDir); logConfig.WriteTo.File(new Serilog.Formatting.Compact.CompactJsonFormatter(), absolutePath, rollingInterval: RollingInterval.Day); } #endif logConfig.WriteTo.Console();Logger logConfig.CreateLogger(); Logger.Information(“LogManager initialized from config. Level: {Level}”, minLevel); } 注意在Unity中完整实现Serilog.Settings.Configuration是复杂的因为涉及程序集动态加载。上述简化方法对于大多数可控的生产环境已经足够。更高级的做法是自行实现一个配置解析器或者将配置放在游戏世界之外如环境变量、命令行参数在游戏启动时通过代码传入。4.2 性能优化关键点日志记录虽好但不当使用会影响性能特别是在每帧都调用的Update方法中。避免在热路径中进行昂贵的参数计算Serilog的消息模板参数是延迟计算的通过捕获委托但如果你在调用日志方法前就进行了复杂的字符串拼接或计算这些开销是无法避免的。不佳实践Log.Information(“Result: “ ExpensiveCalculation());无论日志级别如何ExpensiveCalculation()都会执行。最佳实践利用Serilog的延迟计算和条件日志。// 使用条件日志仅当Debug级别启用时才计算参数 LogManager.Logger.ForContextPlayerCombat().Debug( “Detailed combat state: {CombatState}”, GetCurrentCombatState()); // GetCurrentCombatState() 只在Debug级别启用时调用 // 或者在记录前进行级别检查 if (LogManager.Logger.IsEnabled(LogEventLevel.Debug)) { var state GetCurrentCombatState(); // 仅当需要时才计算 LogManager.Logger.Debug(“State: {State}”, state); }谨慎使用上下文推送LogContext.PushProperty非常有用但频繁地创建和销毁上下文对象尤其是在循环中会产生开销。尽量在较长的逻辑块如一个网络请求的处理周期中使用。选择合适的日志级别生产环境通常将MinimumLevel设置为Information或Warning避免Debug和Verbose级别产生海量日志拖慢I/O。4.3 与Linux系统日志Syslog集成对于需要与现有运维体系集成的Linux服务器将日志发送到Syslog是标准做法。导入Syslog Sink从NuGet下载Serilog.Sinks.Syslog或Serilog.Sinks.SyslogMessages并导入其DLL。配置Syslog Sinkusing Serilog.Sinks.Syslog; // ... 在LoggerConfiguration中 .WriteTo.SyslogUdp(endpoint: “127.0.0.1”, port: 514, format: SyslogFormat.RFC3164, facility: Facility.Local0)或者使用TCP以获得更可靠的传输.WriteTo.TcpSyslog(endpoint: “logserver.example.com”, port: 6514, format: SyslogFormat.RFC5424)配置rsyslog在Linux服务器上你需要在/etc/rsyslog.conf或/etc/rsyslog.d/下添加配置来指定如何处理从你的游戏服务接收到的日志例如写入特定文件或转发到中央日志服务器。5. 常见问题、排查技巧与避坑指南在实际集成过程中你几乎一定会遇到下面这些问题。这里是我的实战记录。5.1 IL2CPP代码裁剪导致的问题问题现象在Editor中运行正常但发布为Linux IL2CPP构建后运行时抛出TypeInitializationException、MissingMethodException或日志完全不输出。根本原因Unity的IL2CPP编译器在构建时会进行积极的代码裁剪Code Stripping以减小包体。它会移除它认为“未被使用”的代码。Serilog大量使用反射和动态类型发现来加载接收器、格式化器等这些代码路径可能在静态分析时被认为未被直接调用从而被裁剪掉。解决方案创建link.xml文件在Assets文件夹下创建或编辑一个名为link.xml的文件。这个文件用于指示IL2CPP链接器保留指定的程序集、命名空间或类型。?xml version“1.0” encoding“UTF-8”? linker assembly fullname“Serilog” preserve“all” / assembly fullname“Serilog.Sinks.Console” preserve“all” / assembly fullname“Serilog.Sinks.File” preserve“all” / assembly fullname“Serilog.Formatting.Compact” preserve“all” / !-- 如果你使用了其他Sink也需要在这里声明 -- assembly fullname“Serilog.Sinks.Syslog” preserve“all” / !-- 保留所有以“Serilog”开头的程序集更激进但更安全 -- !-- assembly fullname“Serilog*” preserve“all” / -- /linkerpreserve“all”会保留该程序集中的所有类型和方法防止被裁剪。确保代码路径被显式引用在游戏启动的代码中如LogManager.Initialize确保你显式地实例化或用到了你需要的Sink和Formatter类型。IL2CPP的静态分析有时需要看到明确的代码引用。例如即使你通过配置字符串指定了CompactJsonFormatter也最好在代码中有一行var _ new Serilog.Formatting.Compact.CompactJsonFormatter();这样的引用。在Player Settings中调整裁剪级别进入Edit - Project Settings - Player - Other Settings找到Managed Stripping Level。尝试将其从High降低到Medium或Low。这虽然会增加构建大小但能有效解决因裁剪过度导致的问题。这是一个很好的调试步骤确认问题后再用link.xml进行精确控制。5.2 Linux文件路径与权限问题问题现象日志文件没有生成或者程序在尝试写入日志时崩溃。排查步骤检查路径是否存在在LogManager初始化时使用Directory.CreateDirectory创建日志目录。并记录下使用的完整路径就像我们在示例代码中用Logger.Information记录Application.persistentDataPath一样。检查写入权限Linux系统对文件权限敏感。确保运行游戏进程的用户对Application.persistentDataPath指向的目录有写入权限。通常在用户主目录下的路径~/.config/unity3d/...是没问题的。但如果你的服务器是以特定服务用户如game-server运行的你需要确保该用户的家目录或你指定的自定义日志目录有相应权限。避免使用根目录或系统目录不要尝试写入/var/log或/tmp以外的系统目录除非你明确配置了权限并以合适的用户身份运行。坚持使用Application.persistentDataPath是最安全的选择。5.3 日志文件过大与滚动策略问题现象磁盘空间被日志文件迅速占满。解决方案使用Serilog内置滚动如示例中所示rollingInterval: RollingInterval.Day和retainedFileCountLimit: 7是基础配置。结合Linux的logrotate对于更精细的控制如按大小滚动、压缩旧日志、发送信号给进程可以配置系统的logrotate。你需要为你的游戏服务创建一个logrotate配置文件如/etc/logrotate.d/my-unity-game/home/gameuser/.config/unity3d/MyCompany/MyGame/logs/*.json { daily missingok rotate 30 compress delaycompress notifempty create 644 gameuser gameuser sharedscripts postrotate # 如果游戏服务支持重新打开日志文件可以在这里发送信号如USR1 # kill -USR1 cat /var/run/my-game.pid endscript }在代码中监控日志目录大小对于长期运行的服务可以在代码中定期检查日志目录的总大小如果超过阈值则删除最旧的日志文件或提高日志级别以减少输出。5.4 异步日志记录与性能瓶颈默认情况下许多Serilog Sink如File Sink是同步的意味着写日志文件的操作会阻塞调用线程。在高频日志记录场景下这可能成为性能瓶颈。解决方案使用异步包装器Async Wrapper。导入Serilog.Sinks.Async包。修改配置用WriteTo.Async()包装你的File Sink或其他可能较慢的Sink。.WriteTo.Async(a a.File( new CompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7 ), bufferSize: 10000) // 设置一个合理的缓冲区大小重要提示异步记录器在应用程序崩溃时缓冲区中未写入的日志可能会丢失。对于需要保证关键错误日志不丢失的场景可以混合使用——将Error及以上级别的日志同时记录到一个同步的、立即刷新的Sink中。5.5 在Unity Editor与独立平台间的行为差异这是一个常见的困惑点需要明确区分。Application.dataPath在Editor中指向项目Assets文件夹。在Linux构建中指向游戏数据目录通常只读。Application.persistentDataPath在Editor中指向%AppData%/../Local/Unity/Editor/下的某个位置Windows或类似位置。在Linux构建中指向~/.config/unity3d/CompanyName/ProductName/。这是存放可写文件如存档、日志的首选位置。控制台输出在Editor的Play模式下输出到Unity Console面板。在Linux独立构建中如果以终端形式运行./MyGame.x86_64输出到终端。如果以后台服务形式运行nohup或 systemd服务需要将标准输出重定向到文件。因此在配置日志路径时一定要使用#if UNITY_EDITOR预编译指令进行区分就像我们在LogManager示例中所做的那样。集成Serilog到Unity Linux游戏开发中看似是多了一步配置但它所带来的结构化日志能力对于后期的问题调试、性能分析、用户行为追踪以及生产环境运维其价值是巨大的。它让游戏日志从“仅供人类阅读的文本”变成了“可供程序高效分析的数据”。一旦你习惯了通过属性来查询日志比如“找出所有伤害超过1000的暴击记录”就再也回不去了。

相关新闻

最新新闻

日新闻

周新闻

月新闻