日志系统 - Snet Docs

📘 Snet.Log 概述

Snet.Log 基于 Serilog 为 Snet 框架提供结构化日志记录。它提供 6 个日志级别(Verbose、Debug、Info、Warn、Error、Fatal),双通道输出(控制台和文件),同步与异步方法变体,以及通过 CoreUnify 系统实现的按实例日志配置。

属性
Snet.Log
命名空间 Snet.Log
目标框架 .NET 8.0 / .NET 10.0
依赖 Serilog 4.4.0, Serilog.Sinks.Console 6.1.1, Serilog.Sinks.File 7.0.0
自动包含 是,通过 Snet.Core
许可证 MIT

▶️ 快速开始

using Snet.Log;

// 基本日志记录
LogHelper.Info("Application started successfully.");
LogHelper.Warn("Disk usage is above 80%.");
LogHelper.Error("Failed to connect to PLC device.");

// 异步日志记录
await LogHelper.InfoAsync("Data acquisition cycle completed.");
await LogHelper.ErrorAsync("Unhandled exception in driver.", ex);

日志输出同时显示在控制台文件中(默认:Logs/snet-{Date}.log)。


⚙️ 安装

dotnet add package Snet.Log

自动包含: Snet.LogSnet.Core 的依赖项,因此如果您的项目已引用 Snet.Core,则无需单独安装。

依赖 版本 用途
Serilog 4.4.0 核心结构化日志引擎
Serilog.Sinks.Console 6.1.1 控制台输出接收器
Serilog.Sinks.File 7.0.0 滚动文件输出接收器

🧠 核心概念

1. LogHelper 静态类

中心化日志 API。所有方法均为静态方法——无需实例化。LogHelper 内部维护一个 Serilog.ILogger 单例实例,在应用程序启动时进行配置。

LogHelper.Info("message");
LogHelper.Error("message", exception);
await LogHelper.DebugAsync("message");

2. 六个日志级别

级别 方法 使用场景
Verbose Verbose() / VerboseAsync() 详细跟踪,内部状态转储
Debug Debug() / DebugAsync() 开发诊断,变量检查
Info Info() / InfoAsync() 正常操作事件(启动、关闭、里程碑)
Warn Warn() / WarnAsync() 潜在问题,降级但仍可用状态
Error Error() / ErrorAsync() 操作失败,异常,数据丢失
Fatal Fatal() / FatalAsync() 应用程序崩溃事件,不可恢复状态

3. LogModel 配置

LogModel 类控制每个应用程序实例的日志行为:

属性 类型 默认值 描述
LogOut bool true 启用/禁用所有日志输出
ConsoleOut bool true 启用/禁用控制台接收器
FileOut bool true 启用/禁用文件接收器
LogLevel int 0 最低日志级别(0=Verbose 至 5=Fatal)
LogPath string "Logs" 日志文件输出目录

4. BeginOperate / EndOperate 集成

每个 Snet 模块调用 LogHelper.BeginOperate() / LogHelper.EndOperate() 将其日志配置注册到 CoreUnify 系统中。这使得可以在没有全局状态冲突的情况下实现按模块的日志设置。

5. 通过 CoreUnify 实现按实例配置

对于不同组件需要不同日志配置的高级场景,请使用 CoreUnify

// 为特定模块实例设置日志配置
await CoreUnify.LogOperateSetAsync(moduleKey, logModel);

// 获取当前日志配置
var config = await CoreUnify.LogOperateGetAsync(moduleKey);

📚 API 参考

LogHelper 方法

所有方法接受一个 string message 参数。Error 和 Fatal 方法额外接受一个可选的 Exception 参数。

同步方法

方法 签名 描述
Info void Info(string message) Info 级别日志记录
Error void Error(string message, Exception? ex = null) Error 级别日志记录
Warn void Warn(string message) Warn 级别日志记录
Debug void Debug(string message) Debug 级别日志记录
Verbose void Verbose(string message) Verbose 级别日志记录
Fatal void Fatal(string message, Exception? ex = null) Fatal 级别日志记录

异步方法

方法 签名 描述
InfoAsync Task InfoAsync(string message) 异步 Info 级别日志记录
ErrorAsync Task ErrorAsync(string message, Exception? ex = null) 异步 Error 级别日志记录
WarnAsync Task WarnAsync(string message) 异步 Warn 级别日志记录
DebugAsync Task DebugAsync(string message) 异步 Debug 级别日志记录
VerboseAsync Task VerboseAsync(string message) 异步 Verbose 级别日志记录
FatalAsync Task FatalAsync(string message, Exception? ex = null) 异步 Fatal 级别日志记录

CoreUnify 日志操作

方法 签名 描述
LogOperateSetAsync Task LogOperateSetAsync(string key, LogModel model) 为模块实例应用日志配置
LogOperateGetAsync Task<LogModel?> LogOperateGetAsync(string key) 获取模块实例的日志配置

💻 代码示例

基本日志记录

using Snet.Log;

LogHelper.Info("=== Snet DAQ Service Starting ===");
LogHelper.Debug($"Configuration path: {configPath}");
LogHelper.Info($"Loaded {deviceCount} device definitions.");
LogHelper.Warn("Sensor SN-00321 has not reported in 60 seconds.");
LogHelper.Error($"Failed to write value to address {addr}: {er.Message}");
LogHelper.Fatal($"Unrecoverable error in main loop. Shutting down.", ex);

带异常的结构化日志

try
{
    var data = await modbus.ReadHoldingRegistersAsync(deviceId, startAddress, count);
    LogHelper.Info($"Read {data.Length} registers from device {deviceId}");
}
catch (TimeoutException ex)
{
    LogHelper.Error($"Device {deviceId} timeout: {ex.Message}", ex);
}
catch (Exception ex)
{
    LogHelper.Fatal($"Unexpected error communicating with device {deviceId}", ex);
    // 启动恢复或关闭流程
}

在 DAQ 驱动上下文中记录日志

public class ModbusTcpDriver : CommunicationAbstract
{
    private readonly string _driverKey;

    public override async Task<OperateResult> OnAsync(BasicsAbstract basics)
    {
        // 配置实例特定的日志
        var logModel = new LogModel
        {
            LogOut = true,
            ConsoleOut = true,
            FileOut = true,
            LogLevel = 0, // 驱动开发阶段使用 Verbose 级别
            LogPath = Path.Combine("Logs", "Drivers", _driverKey)
        };
        await CoreUnify.LogOperateSetAsync(_driverKey, logModel);

        LogHelper.Info($"[{_driverKey}] Driver initializing...");
        var result = await base.OnAsync(basics);
        LogHelper.Info($"[{_driverKey}] Driver init result: {result.Status}");
        return result;
    }

    public override async Task<OperateResult<byte[]>> ReadAsync(string address, ushort length)
    {
        LogHelper.Debug($"[{_driverKey}] READ request: address={address}, length={length}");
        var result = await base.ReadAsync(address, length);
        if (!result.Status)
            LogHelper.Error($"[{_driverKey}] READ failed: {result.Message}");
        else
            LogHelper.Verbose($"[{_driverKey}] READ success: {result.ResultData!.Length} bytes");
        return result;
    }

    public override async Task<OperateResult> WriteAsync(string address, byte[] data)
    {
        LogHelper.Debug($"[{_driverKey}] WRITE request: address={address}, length={data.Length}");
        var result = await base.WriteAsync(address, data);
        if (!result.Status)
            LogHelper.Error($"[{_driverKey}] WRITE failed: {result.Message}");
        return result;
    }
}

后台服务的异步日志

public class DataAcquisitionService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await LogHelper.InfoAsync("Data acquisition service started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await AcquireDataCycleAsync();
                await LogHelper.DebugAsync("Acquisition cycle completed successfully.");
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                await LogHelper.InfoAsync("Service stopping by cancellation request.");
                break;
            }
            catch (Exception ex)
            {
                await LogHelper.ErrorAsync("Acquisition cycle failed.", ex);
            }

            await Task.Delay(1000, stoppingToken);
        }

        await LogHelper.InfoAsync("Data acquisition service stopped.");
    }

    private async Task AcquireDataCycleAsync() { /* ... */ }
}

❓ 常见问题

1. 如何更改最低日志级别?

LogModel 中设置 LogLevel 属性,然后通过 CoreUnify.LogOperateSetAsync() 应用:

LogLevel 显示的最低级别
0 Verbose(全部)
1 Debug 及以上
2 Info 及以上
3 Warn 及以上
4 Error 及以上
5 仅 Fatal
var model = new LogModel { LogLevel = 3 }; // 仅显示 Warn、Error、Fatal
await CoreUnify.LogOperateSetAsync("MyModule", model);

2. 如何添加自定义 Serilog 接收器?

LogHelper 通过 Serilog 内部配置接收器。要添加自定义接收器(如 Seq、Elasticsearch 或数据库),请在 LogHelper 初始化之前自定义 Serilog 管道的 Serilog.LoggerConfiguration。有关高级接收器配置的指导,请联系 Snet 框架团队。

3. 日志文件存储在哪里?

默认情况下,日志文件写入应用程序工作目录下的 Logs/ 目录,文件名遵循 snet-{yyyyMMdd}.log 模式。LogModel 上的 LogPath 属性控制输出目录:

var model = new LogModel { LogPath = @"D:\AppLogs\Snet" };

4. 应该使用同步还是异步日志方法?

异步上下文中(如 ASP.NET Core、后台服务和异步事件处理器),应优先使用异步方法(InfoAsyncErrorAsync 等)。同步方法适用于控制台应用程序同步代码路径和无法使用 async 的构造函数。两种变体以相同的格式写入相同的接收器。

5. Snet.Log 是线程安全的吗?

是的。Serilog 的 Log.Logger 是完全线程安全的。LogHelper 使用静态单例包装它,使所有日志调用在任何线程或任务中都是安全的。

6. 日志记录会影响性能吗?

通过 Serilog 的结构化日志设计为低开销。文件接收器使用异步缓冲写入器。对于生产环境中的高频 debug/verbose 日志记录,请提高最低 LogLevel 以避免过多的 I/O。仅在开发或排查问题期间使用 VerboseDebug 级别。


📅 版本历史

日期 版本 变更
2026-07-23 当前版本;Serilog 4.4.0,.NET 8/10 支持