目录
1. 概述
1.1 什么是 Related_Log?
Related_Log 是一个专为 Windows 桌面应用程序设计的日志和配置管理类库。它提供三大核心功能:
| 功能模块 | 说明 | 典型用途 |
|---|---|---|
| INI配置管理 | 读写 INI 格式配置文件 | 保存设备参数、通讯配置、用户设置 |
| 文本日志系统 | 记录系统运行时的调试、警告、错误信息 | 问题诊断、系统监控、审计追踪 |
| XML操作日志 | 记录用户操作行为(谁在什么时候做了什么) | 安全审计、操作追溯、责任追溯 |
1.2 核心特性
- 高性能异步写入:日志写入采用队列批处理机制,不阻塞主线程
- 零依赖:仅依赖 .NET Framework 基础类库
- 线程安全:所有服务均支持多线程并发调用
- 自动分类存储:日志按级别和日期自动分目录、分文件存储
- 易于集成:单例模式设计,一行代码即可调用
1.3 文件目录结构
应用程序根目录/
├── config.ini # INI配置文件(手动或通过API创建)
├── logs/ # 文本日志目录(自动创建)
│ ├── INFO/ # 信息级别日志
│ │ ├── 2026-06-10.log # 按日期分文件
│ │ └── 2026-06-11.log
│ ├── WARN/ # 警告级别日志
│ ├── ERROR/ # 错误级别日志
│ ├── DEBUG/ # 调试级别日志
│ └── FATAL/ # 致命错误级别日志
└── logs_xml/ # XML操作日志目录(自动创建)
├── 2026-06-10_log.xml # 按日期分文件
└── 2026-06-11_log.xml
2. 架构设计
2.1 设计模式
本类库综合运用了多种设计模式:
| 设计模式 | 应用位置 | 作用 |
|---|---|---|
| 单例模式 | SystemConfigService |
确保全局唯一访问入口 |
| 外观模式 | SystemConfigService |
隐藏内部复杂性,提供简洁API |
| 生产者-消费者模式 | LogQueueProcessor |
异步队列处理,提升性能 |
| 观察者模式 | LogMessage/XmlMessage 事件 |
实时推送日志到UI |
| 策略模式 | 日志保留策略 | 灵活配置清理规则 |
2.2 类图
┌─────────────────────────────────────────────────────────────────┐
│ SystemConfigService (单例) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Config │ │ Log │ │ Xml │ │
│ │ (属性公开) │ │ (属性公开) │ │ (属性公开) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────────┘ │
└─────────┼─────────────────┼────────────────────┼────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────┐ ┌─────────────────────┐
│IniConfigService│ │LogService │ │XmlLogServiceWrapper │
│ (内部实现) │ │ (封装类) │ │ (封装类) │
└─────────────────┘ └──────┬──────┘ └──────────┬──────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ LogQueueProcessor │
│ (队列处理器 - 后台线程) │
│ ┌─────────────────────────┐ │
│ │ _logQueue (文本日志队列) │ │
│ │ _xmlQueue (XML日志队列) │ │
│ └─────────────────────────┘ │
└──────────────┬──────────────────┘
│
┌──────────────┴──────────────────┐
▼ ▼
┌───────────────┐ ┌──────────────┐
│LogFileService │ │XmlLogService │
│ (磁盘写入) │ │ (磁盘写入) │
└───────────────┘ └──────────────┘
2.3 数据流程图
┌──────────┐
│ 用户代码 │
└─────┬────┘
│ 调用API
▼
┌─────────────────────────────────────┐
│ SystemConfigService.Instance │
│ .Log.Info("消息", "来源") │
│ .Xml.Log(sender, "用户名") │
│ .Config.Read("Section", "Key") │
└─────────────────────────────────────┘
│
├─→ INI读写:立即返回
│
├─→ Log/Xml写入:
│ 1. 创建请求对象
│ 2. 放入内存队列(瞬间完成)
│ 3. 触发事件(UI实时显示)
│ 4. 后台线程批量写入文件
│
▼
┌─────────────────────────────────────┐
│ 磁盘文件 │
│ config.ini / logs/ / logs_xml/ │
└─────────────────────────────────────┘
3. 核心设计模式原理
本章目标 :深入理解类库使用的两大核心设计模式------单例模式 和生产者-消费者模式(消息队列),帮助开发者掌握其原理并能够应用到自己的项目中。
3.1 单例模式(Singleton Pattern)原理详解
3.1.1 什么是单例模式?
单例模式 是一种创建型设计模式,它确保一个类在整个应用程序生命周期中只能有一个实例,并提供一个全局访问点来获取该实例。
生活中的类比:
- 打印机管理器:办公室里可能有多台电脑,但只有一台打印机。所有电脑通过同一个"打印机管理器"来排队打印任务。
- Windows任务管理器:无论你打开多少次任务管理器,系统只允许一个窗口存在。
3.1.2 为什么需要单例模式?
在日志系统中使用单例模式的理由:
| 理由 | 说明 |
|---|---|
| 资源共享 | 日志文件是共享资源,多个实例同时写入会导致文件冲突 |
| 状态一致性 | 配置信息需要在整个程序中保持一致 |
| 性能优化 | 避免重复创建对象,节省内存 |
| 全局访问 | 任何地方都能方便地调用,无需传递引用 |
3.1.3 单例模式的实现方式
方式1:简单单例(非线程安全)
csharp
public class SimpleSingleton
{
private static SimpleSingleton _instance; // 静态变量存储唯一实例
private SimpleSingleton() // 私有构造函数,防止外部创建实例
{
}
public static SimpleSingleton Instance // 全局访问点
{
get
{
if (_instance == null) // 如果实例不存在,创建它
_instance = new SimpleSingleton();
return _instance;
}
}
}
问题 :多线程环境下,两个线程可能同时通过 if (_instance == null) 检查,导致创建多个实例!
方式2:线程安全的单例(双重检查锁定)
csharp
public class ThreadSafeSingleton
{
private static ThreadSafeSingleton _instance;
private static readonly object _lock = new object(); // 锁对象
private ThreadSafeSingleton()
{
}
public static ThreadSafeSingleton Instance
{
get
{
if (_instance == null) // 第一次检查(未加锁,提高性能)
{
lock (_lock) // 加锁
{
if (_instance == null) // 第二次检查(加锁后)
{
_instance = new ThreadSafeSingleton();
}
}
}
return _instance;
}
}
}
工作原理:
- 第一次检查:如果实例已存在,直接返回,避免不必要的锁等待
- 加锁:确保同一时间只有一个线程能进入创建代码
- 第二次检查:防止多个线程同时通过第一次检查后,在锁内重复创建
流程图:
多个线程同时调用 Instance
│
▼
_instance == null? ←───┐
│ │
是/Yes 否/No │ 直接返回已存在的实例
│ │
▼ │
获取锁 _lock │
│ │
▼ │
等待获取锁 │
│ │
▼ │
获得锁后再次检查 ───────┘
│
_instance == null?
│
是/Yes → 创建新实例 → 释放锁 → 返回实例
否/No → 释放锁 → 返回现有实例
方式3:本类库使用的写法(简化双重检查)
csharp
public partial class SystemConfigService : IDisposable
{
private static SystemConfigService _instance;
private static readonly object _lock = new object();
private SystemConfigService() // 私有构造函数
{
// 初始化各个服务
Config = new IniConfigService();
// ...
}
public static SystemConfigService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
// 使用 ?? 运算符简化写法
_instance = _instance ?? new SystemConfigService();
}
}
return _instance;
}
}
}
代码解读:
-
_instance = _instance ?? new SystemConfigService()等价于:csharpif (_instance == null) _instance = new SystemConfigService();
3.1.4 单例模式在类库中的应用
csharp
// ========== 任何地方都可以这样调用 ==========
// 不需要 new,不需要传递引用
SystemConfigService.Instance.Log.Info("消息", "来源");
// ========== 在不同类中使用 ==========
public class FrmMain
{
public void LoadData()
{
// 直接使用,无需传递
SystemConfigService.Instance.Log.Info("加载开始", "FrmMain");
}
}
public class DataService
{
public void SaveData()
{
// 同一个实例,共享状态
SystemConfigService.Instance.Log.Info("保存开始", "DataService");
}
}
3.1.5 单例模式的优缺点
| 优点 | 缺点 |
|---|---|
| ✅ 全局唯一访问点 | ❌ 难以单元测试(全局状态) |
| ✅ 节省内存资源 | ❌ 违反单一职责原则(管理自身+业务逻辑) |
| ✅ 延迟初始化(用到时才创建) | ❌ 隐藏依赖关系(类之间的耦合不明显) |
| ✅ 线程安全(正确实现时) | ❌ 可能被滥用(全局变量替代品) |
3.1.6 何时使用单例模式?
适用场景:
- ✅ 日志服务、配置管理器、数据库连接池
- ✅ 线程池、缓存管理器、打印机后台处理程序
不适用场景:
- ❌ 普通的业务对象(如用户、订单)
- ❌ 需要多实例的对象(如网络连接)
3.2 生产者-消费者模式(消息队列)原理详解
3.2.1 什么是生产者-消费者模式?
生产者-消费者模式是一种并发设计模式,它解耦了"生产数据"和"处理数据"两个过程,通过一个**缓冲区(队列)**来连接它们。
生活中的类比:
- 餐厅后厨 :
- 生产者:厨师(不断炒菜)
- 队列:出餐口(放菜的地方)
- 消费者:服务员(取菜送给客人)
- 快递中转站 :
- 生产者:快递员(不断送来包裹)
- 队列:分拣传送带
- 消费者:派送员(取走包裹派送)
3.2.2 为什么需要消息队列?
场景对比:
| 对比项 | 直接写入磁盘(传统方式) | 消息队列(本类库方式) |
|---|---|---|
| 写入方式 | 每次调用立即写文件 | 先放入队列,后台批量写 |
| 主线程阻塞 | 每次写入可能耗时 5-50ms | 入队耗时 <0.1ms |
| 100条日志总耗时 | 500-5000ms | 入队 <10ms,后台异步处理 |
| 用户体验 | 界面可能卡顿 | 界面流畅无感 |
| 磁盘I/O次数 | 100次(每条1次) | 1-2次(批量) |
核心优势:
- 解耦:生产者和消费者互不依赖,可以独立变化
- 削峰:处理突发大量请求时,队列起到缓冲作用
- 异步:主线程快速返回,耗时操作在后台执行
- 批量处理:合并多次写入为一次,减少磁盘I/O
3.2.3 消息队列的工作原理
核心组件
┌─────────────────────────────────────────────────────────────┐
│ 生产者-消费者模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ 生产者 │ 入队 │ 消息队列 │ 出队 │ 消费者 │ │
│ │(多个线程)│ ───────→│ (缓冲区) │ ───────→│(后台线程)│ │
│ └──────────┘ └───────────┘ └──────────┘ │
│ │ │ │ │
│ │ │ │ │
│ 用户代码 内存队列 批量写入文件 │
│ 调用Log.Info() ConcurrentQueue LogFileService │
│ │
└─────────────────────────────────────────────────────────────┘
时序图
时间线 ──────────────────────────────────────────────────────→
主线程(生产者): 后台线程(消费者):
│ │
│ Log.Info("消息1") │
├─→ 创建LogRequest对象 │
├─→ 入队(瞬间完成) │
├─→ 立即返回 <0.1ms ──────────────────→ 休眠中...
│ │
│ Log.Info("消息2") │
├─→ 创建LogRequest对象 │
├─→ 入队 │
├─→ 立即返回 │
│ │
│ Log.Info("消息3") │
├─→ 创建LogRequest对象 │
│ ...(主线程继续运行) │
│ │ 1秒后或队列满时被唤醒
│ ├─→ 从队列取出100条
│ ├─→ 批量写入文件(一次性)
│ ├─→ 写入完成
│ └─→ 继续休眠
3.2.4 本类库中的队列实现
队列处理器核心代码
csharp
internal class LogQueueProcessor : IDisposable
{
// 线程安全的队列(.NET 提供)
private readonly ConcurrentQueue<LogRequest> _logQueue;
private readonly ConcurrentQueue<XmlLogRequest> _xmlQueue;
// 后台线程
private Thread _logThread;
private Thread _xmlThread;
// 控制标志
private bool _isRunning;
private readonly ManualResetEventSlim _logEvent; // 通知信号
// 批量写入配置
private const int BATCH_SIZE = 100; // 每次批量写入100条
private const int FLUSH_INTERVAL_MS = 1000; // 或者每1秒写一次
public LogQueueProcessor(
Action<LogRequest[]> processLogBatch,
Action<XmlLogRequest[]> processXmlBatch)
{
_logQueue = new ConcurrentQueue<LogRequest>();
_xmlQueue = new ConcurrentQueue<XmlLogRequest>();
_logEvent = new ManualResetEventSlim(false);
// 启动后台线程
Start();
}
// ========== 生产者方法(主线程调用) ==========
public void EnqueueLog(LogRequest request)
{
if (request == null) return;
_logQueue.Enqueue(request); // 入队(线程安全,<0.01ms)
_logEvent.Set(); // 通知后台线程"有新数据"
}
// ========== 消费者方法(后台线程执行) ==========
private void ProcessLogQueue()
{
while (_isRunning)
{
// 等待信号或超时(1秒)
_logEvent.Wait(FLUSH_INTERVAL_MS);
_logEvent.Reset();
// 收集一批日志
var batch = new LogRequest[BATCH_SIZE];
int count = 0;
// 从队列中取出(最多100条)
while (count < BATCH_SIZE && _logQueue.TryDequeue(out var request))
{
batch[count++] = request;
}
// 如果有日志,批量处理
if (count > 0)
{
Array.Resize(ref batch, count); // 调整数组大小
_processLogBatch(batch); // 调用写入方法
}
}
}
}
关键技术点解析
1. ConcurrentQueue - 线程安全队列
csharp
// 普通队列(非线程安全)
Queue<LogRequest> queue = new Queue<LogRequest>();
queue.Enqueue(item); // 多线程同时调用会出错
var item = queue.Dequeue();
// 线程安全队列
ConcurrentQueue<LogRequest> queue = new ConcurrentQueue<LogRequest>();
queue.Enqueue(item); // 多线程同时调用安全 ✓
queue.TryDequeue(out item); // 尝试取出,失败返回false
为什么需要线程安全?
- 主线程(生产者)在入队
- 后台线程(消费者)在出队
- 两个线程同时操作同一个队列,必须使用线程安全的队列
2. ManualResetEventSlim - 线程间通信
csharp
// 创建事件(初始状态:无信号)
var event = new ManualResetEventSlim(false);
// 后台线程:等待信号
event.Wait(1000); // 等待1000ms或收到信号
event.Reset(); // 重置为无信号状态
// 主线程:发送信号
event.Set(); // 通知后台线程"醒来工作"
工作原理:
后台线程 事件状态 主线程
│ │ │
│ Wait() □ 无信号 │
│ (阻塞) │ │
│ │ │ Enqueue()
│ │ │
│ │ ├─ Set()
│ │ │
│ ← 醒来! ■ 有信号 │
│ 开始工作 │ │
│ │ │
│ Reset() □ 无信号 │
│ Wait() │ │
│ (阻塞) │ │
3. 批量处理 - 性能优化的关键
csharp
// ❌ 效率低:每次写一条
foreach (var log in logs)
{
File.AppendAllText("file.log", log.ToString()); // 100次磁盘I/O
}
// ✅ 效率高:批量写入
var content = string.Join("\r\n", logs);
File.AppendAllText("file.log", content); // 1次磁盘I/O
性能对比(100条日志):
- 逐条写入:~500ms(100次磁盘I/O)
- 批量写入:~50ms(1次磁盘I/O)
3.2.5 完整数据流程图
┌──────────────────────────────────────────────────────────────────┐
│ 用户代码 │
│ SystemConfigService.Instance.Log.Info("设备连接成功", "PLC"); │
└────────────────────────────┬─────────────────────────────────────┘
│ 调用
▼
┌──────────────────────────────────────────────────────────────────┐
│ LogService.Log() │
│ 1. 创建 LogRequest 对象(包含级别、消息、来源、时间戳) │
│ 2. 调用 _queueProcessor.EnqueueLog(request) ← 瞬间完成 │
└────────────────────────────┬─────────────────────────────────────┘
│ 入队
▼
┌──────────────────────────────────────────────────────────────────┐
│ ConcurrentQueue<LogRequest> │
│ 内存中的日志队列 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Log1 │ │Log2 │ │Log3 │ │Log4 │ │Log5 │ │ ... │ ... │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
└────────────────────────────┬─────────────────────────────────────┘
│ 后台线程定时取出(100条或1秒)
▼
┌──────────────────────────────────────────────────────────────────┐
│ LogFileService.WriteBatch() │
│ 1. 按日期和级别分组 │
│ 2. 拼接成字符串(用 \r\n 连接) │
│ 3. File.AppendAllText() 一次性写入文件 │
└────────────────────────────┬─────────────────────────────────────┘
│ 写入
▼
┌──────────────────────────────────────────────────────────────────┐
│ 磁盘日志文件 │
│ logs/INFO/2026-06-11.log │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 2026-06-11 14:30:25.1234|INFO|PLC|设备连接成功 │ │
│ │ 2026-06-11 14:30:26.5678|INFO|Modbus|读取寄存器 │ │
│ │ ... │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
3.2.6 队列的优缺点与权衡
| 优点 | 缺点 | 权衡方案 |
|---|---|---|
| ✅ 主线程不被阻塞 | ❌ 程序崩溃可能丢失队列中的数据 | 程序退出时调用 Flush() |
| ✅ 批量写入,减少I/O | ❌ 增加内存消耗 | 限制队列最大长度 |
| ✅ 处理突发流量 | ❌ 日志可能有延迟 | 设置合理的刷新间隔 |
| ✅ 线程安全 | ❌ 调试难度增加 | 添加队列大小监控 |
3.2.7 何时使用消息队列?
适用场景:
- ✅ 日志记录
- ✅ 消息发送(邮件、短信)
- ✅ 文件上传
- ✅ 数据库批量写入
- ✅ 任何"生产快、消费慢"的场景
不适用场景:
- ❌ 需要立即确认的操作(如支付)
- ❌ 数据量极小,直接处理更简单
- ❌ 消息不能丢失且无法容忍延迟
3.3 设计模式组合运用
本类库巧妙地将单例模式 和生产者-消费者模式组合使用:
┌─────────────────────────────────────────────────────────────────┐
│ 组合架构示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ SystemConfigService (单例) │ │
│ │ 确保全局只有一个入口,管理所有日志和配置服务 │ │
│ └─────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Config服务 │ │ Log服务 │ │ Xml服务 │ │
│ │ (立即执行) │ │ (入队返回) │ │ (入队返回) │ │
│ └────────────┘ └──────┬─────┘ └──────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────┐ │
│ │ LogQueueProcessor │ │
│ │ (生产者-消费者模式) │ │
│ │ 统一的队列处理中心 │ │
│ └─────────────┬──────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │LogFileService│ │XmlLogService│ │
│ │ 消费者:批量写入 │ │ 消费者:批量写入 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
组合的优势:
- 单例提供统一入口,调用简单
- 队列提供异步处理,性能优秀
- 两者结合,既好用又高效
4. 核心概念
3.1 日志级别(LogLevel)
文本日志支持 5 个级别,从低到高:
| 级别 | 枚举值 | 说明 | 适用场景 |
|---|---|---|---|
| 信息 | LogLevel.Info |
正常运行信息 | 连接成功、数据更新、流程完成 |
| 警告 | LogLevel.Warn |
潜在问题 | 参数异常、重连尝试、接近阈值 |
| 错误 | LogLevel.Error |
错误但可恢复 | 通讯失败、文件读写错误 |
| 调试 | LogLevel.Debug |
调试信息 | 详细数据、中间状态 |
| 致命 | LogLevel.Fatal |
严重错误 | 程序无法继续运行 |
代码示例:
csharp
SystemConfigService.Instance.Log.Info("设备连接成功", "FrmMain");
SystemConfigService.Instance.Log.Warn("通讯延迟超过阈值", "CommService");
SystemConfigService.Instance.Log.Error("读取寄存器失败", ex, "ModbusRTU");
SystemConfigService.Instance.Log.Debug("发送数据: 01 03 00 00 00 0A", "CommService");
SystemConfigService.Instance.Log.Fatal("系统崩溃,无法启动", ex, "Program");
3.2 日志保留策略(LogRetentionDays)
用于控制日志文件的清理周期:
| 枚举值 | 天数 | 说明 |
|---|---|---|
Never |
-1 | 不清理,永久保留 |
Days7 |
7 | 保留 7 天 |
Days30 |
30 | 保留 30 天(默认) |
Days60 |
60 | 保留 60 天 |
Days90 |
90 | 保留 90 天 |
Days180 |
180 | 保留 180 天 |
3.3 INI配置文件结构
INI 文件采用经典的"节-键-值"结构:
ini
[Section1]
Key1=Value1
Key2=Value2
[Section2]
Key1=Value1
Key2=Value2
示例:
ini
[ModbusTCP]
IP=192.168.1.100
Port=502
SlaveId=1
[Storage]
Save_Log=30天
Save_Xml=30天
5. 详细API文档
4.1 SystemConfigService - 统一访问入口
这是类库的主入口,采用单例模式,通过 SystemConfigService.Instance 访问。
4.1.1 属性
| 属性 | 类型 | 说明 |
|---|---|---|
Config |
IniConfigService |
INI配置服务实例 |
Log |
LogService |
文本日志服务实例 |
Xml |
XmlLogServiceWrapper |
XML操作日志服务实例 |
4.1.2 方法
日志清理:
csharp
// 方式1:使用整数天数
CleanOldLogs(int logRetentionDays = -1, int xmlRetentionDays = -1)
// 方式2:使用枚举(推荐)
CleanOldLogs(LogRetentionDays logRetention, LogRetentionDays xmlRetention)
统计信息:
csharp
// 获取队列大小(用于监控)
(int logQueueSize, int xmlQueueSize) GetQueueSize()
// 获取日志占用空间
long GetTotalLogSize()
// 获取日志文件数量
(int logCount, int xmlCount) GetLogFileCount()
资源释放:
csharp
void Dispose()
4.1.3 事件
| 事件 | 参数类型 | 说明 |
|---|---|---|
LogMessage |
EventHandler<LogEventArgs> |
文本日志消息事件 |
XmlMessage |
EventHandler<XmlLogEventArgs> |
XML日志消息事件 |
事件订阅示例:
csharp
SystemConfigService.Instance.LogMessage += OnLogReceived;
SystemConfigService.Instance.XmlMessage += OnXmlLogReceived;
private void OnLogReceived(object sender, LogEventArgs e)
{
// e.Level - 日志级别
// e.Message - 日志消息
// e.Source - 来源
// e.Timestamp - 时间戳
}
private void OnXmlLogReceived(object sender, XmlLogEventArgs e)
{
// e.Username - 用户名
// e.Action - 操作
// e.Details - 详情
// e.Timestamp - 时间戳
}
4.2 LogService - 文本日志服务
通过 SystemConfigService.Instance.Log 访问。
4.2.1 方法
基本方法:
csharp
// 记录信息
void Info(string message, string source = "")
// 记录警告
void Warn(string message, string source = "")
// 记录错误(无异常)
void Error(string message, string source = "")
// 记录错误(带异常)
void Error(string message, Exception ex, string source = "")
// 记录调试信息
void Debug(string message, string source = "")
// 记录致命错误(无异常)
void Fatal(string message, string source = "")
// 记录致命错误(带异常)
void Fatal(string message, Exception ex, string source = "")
// 核心方法(直接指定级别)
void Log(LogLevel level, string message, string source = "", Exception ex = null)
4.2.2 使用示例
csharp
// 简单记录
SystemConfigService.Instance.Log.Info("应用程序启动", "Program");
// 带来源记录
SystemConfigService.Instance.Log.Warn("配置文件不存在,使用默认值", "ConfigService");
// 记录异常
try
{
File.Delete("somefile.txt");
}
catch (Exception ex)
{
SystemConfigService.Instance.Log.Error("删除文件失败", ex, "FileService");
}
// 使用核心方法
SystemConfigService.Instance.Log.Log(
LogLevel.Info,
"用户登录成功",
"LoginService"
);
4.2.3 日志文件格式
文本日志文件采用管道符分隔格式:
时间戳|级别|来源|消息
示例:
2026-06-11 14:30:25.1234|INFO|Program|应用程序启动
2026-06-11 14:30:26.4567|WARN|ConfigService|配置文件不存在,使用默认值
2026-06-11 14:30:27.7890|ERROR|ModbusRTU|读取寄存器失败 | 异常: TimeoutException: 连接超时 | 堆栈: at ModbusRTU.Read()...
4.3 XmlLogServiceWrapper - XML操作日志服务
通过 SystemConfigService.Instance.Xml 访问。
4.3.1 方法
csharp
// 自动提取控件信息记录(推荐)
void Log(object sender, string username, string additionalAction = null, string details = null)
// 直接指定参数记录
void Log(string username, string action, string details = "")
4.3.2 使用示例
csharp
// 在按钮点击事件中使用(推荐)
private void btnSave_Click(object sender, EventArgs e)
{
// 自动提取控件名称、文本、页面信息
SystemConfigService.Instance.Xml.Log(sender, Environment.UserName);
// 或者附加额外信息
SystemConfigService.Instance.Xml.Log(
sender,
Environment.UserName,
additionalAction: "保存配方",
details: "配方名称: Recipe001"
);
}
// 直接记录
SystemConfigService.Instance.Xml.Log(
"Admin",
"UserLogin",
"IP: 192.168.1.100"
);
4.3.3 自动提取的信息
当使用 Log(object sender, ...) 重载时,系统会自动提取:
| 信息 | 来源 | 示例 |
|---|---|---|
| 控件名称 | Control.Name |
btnSave |
| 控件文本 | Control.Text |
保存 |
| 控件标签 | Control.Tag |
SaveRecipe |
| 页面名称 | 查找父级 UserControl |
Page_Recipe_01List |
| 页面标题 | Page.Text |
配方列表 |
生成的 Action 格式:控件名-控件文本(Tag信息)-附加操作-被按下
4.3.4 XML日志文件格式
XML日志文件使用 .NET XML 序列化,结构如下:
xml
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfXmlLogEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<XmlLogEntry>
<Timestamp>2026-06-11 14:30:25.123456</Timestamp>
<CurrentUser>Admin</CurrentUser>
<Action>btnSave-保存(SaveRecipe)-被按下</Action>
<Details>页面:Page_Recipe_01List(配方列表); 配方名称: Recipe001</Details>
</XmlLogEntry>
<XmlLogEntry>
<Timestamp>2026-06-11 14:31:10.654321</Timestamp>
<CurrentUser>Admin</CurrentUser>
<Action>UserLogin</Action>
<Details>IP: 192.168.1.100</Details>
</XmlLogEntry>
</ArrayOfXmlLogEntry>
4.4 IniConfigService - INI配置服务
通过 SystemConfigService.Instance.Config 访问。
4.4.1 读取方法
csharp
// 读取字符串
string Read(string section, string key, string defaultValue = "")
// 读取整数
int ReadInt(string section, string key, int defaultValue = 0)
// 读取长整数
long ReadLong(string section, string key, long defaultValue = 0)
// 读取布尔值
bool ReadBool(string section, string key, bool defaultValue = false)
// 读取浮点数
double ReadDouble(string section, string key, double defaultValue = 0.0)
// 读取单精度浮点数
float ReadFloat(string section, string key, float defaultValue = 0.0f)
// 读取整个Section的所有键值对
Dictionary<string, string> ReadSection(string section)
// 获取所有Section名称
List<string> GetSections()
4.4.2 写入方法
csharp
// 写入单个配置
bool Write(string section, string key, object value)
// 批量写入一个Section的多个键值对
void WriteSection(string section, Dictionary<string, object> keyValues)
4.4.3 删除方法
csharp
// 删除指定键
bool DeleteKey(string section, string key)
// 删除指定Section
bool DeleteSection(string section)
4.4.4 检查方法
csharp
// 检查键是否存在
bool KeyExists(string section, string key)
// 检查Section是否存在
bool SectionExists(string section)
4.4.5 属性
csharp
// 获取INI文件路径
string FilePath { get; }
4.4.6 使用示例
csharp
var config = SystemConfigService.Instance.Config;
// ========== 读取 ==========
// 读取字符串
string ip = config.Read("ModbusTCP", "IP", "127.0.0.1");
// 读取整数
int port = config.ReadInt("ModbusTCP", "Port", 502);
// 读取布尔值
bool activate = config.ReadBool("ModbusTCP", "Activate", false);
// 读取整个Section
var allSettings = config.ReadSection("ModbusTCP");
// 结果: {"IP": "192.168.1.100", "Port": "502", "SlaveId": "1"}
// ========== 写入 ==========
// 写入单个配置
config.Write("ModbusTCP", "IP", "192.168.1.100");
config.Write("ModbusTCP", "Port", 502);
config.Write("ModbusTCP", "Activate", true);
// 批量写入
config.WriteSection("ModbusTCP", new Dictionary<string, object>
{
{ "IP", "192.168.1.100" },
{ "Port", 502 },
{ "SlaveId", 1 },
{ "Activate", true }
});
// ========== 删除 ==========
config.DeleteKey("ModbusTCP", "OldKey");
config.DeleteSection("OldSection");
// ========== 检查 ==========
bool hasKey = config.KeyExists("ModbusTCP", "IP"); // true
bool hasSection = config.SectionExists("ModbusTCP"); // true
4.5 日志清理服务
通过 SystemConfigService.Instance.CleanOldLogs(...) 访问。
4.5.1 清理结果类
csharp
public class CleanResult
{
public bool Success { get; set; } // 是否成功
public DateTime CleanTime { get; set; } // 清理时间
public int LogFilesDeleted { get; set; } // 删除的Log文件数
public long LogSpaceFreed { get; set; } // 释放的Log空间(字节)
public int XmlFilesDeleted { get; set; } // 删除的XML文件数
public long XmlSpaceFreed { get; set; } // 释放的XML空间(字节)
public List<string> LogErrors { get; set; } // Log清理错误列表
public List<string> XmlErrors { get; set; } // XML清理错误列表
public int TotalFilesDeleted { get; } // 总删除文件数
public long TotalSpaceFreed { get; } // 总释放空间
public string GetSummary() { } // 格式化摘要
}
4.5.2 使用示例
csharp
// 清理30天前的文本日志和XML日志
var result = SystemConfigService.Instance.CleanOldLogs(
LogRetentionDays.Days30, // 文本日志保留30天
LogRetentionDays.Days60 // XML日志保留60天
);
if (result.Success)
{
MessageBox.Show(result.GetSummary());
// 示例输出: "清理完成: 删除 15 个文件, 释放 25.6 MB 空间"
}
// 或使用整数
var result2 = SystemConfigService.Instance.CleanOldLogs(
logRetentionDays: 30,
xmlRetentionDays: 60
);
6. 集成指南
5.1 将类库打包为DLL
步骤1:创建类库项目
- 在 Visual Studio 中创建新的 类库(.NET Framework) 项目
- 项目命名:
Related_Log - 目标框架:.NET Framework 4.8(或你的项目使用的版本)
步骤2:复制源文件
将以下文件复制到类库项目中:
Related_Log/
├── Models/
│ └── LogModels.cs
├── Service/
│ ├── Related_Log/
│ │ ├── LogFileService.cs
│ │ └── LogQueueProcessor.cs
│ ├── Related_Xml/
│ │ └── XmlLogService.cs
│ ├── Related_Ini/
│ │ └── IniConfigService.cs
│ └── Related_Clean/
│ └── LogCleanService.cs
└── SystemConfigService.cs
步骤3:添加引用
项目需要引用:
System(默认)System.Core(默认)System.Windows.Forms(需要添加)
步骤4:编译生成DLL
- 选择 Release 配置
- 点击 生成 → 生成解决方案
- 在
bin\Release\目录下找到Related_Log.dll
5.2 在新项目中集成
步骤1:添加DLL引用
- 在目标项目中,右键 引用 → 添加引用
- 选择 浏览 ,找到
Related_Log.dll - 勾选并确定
步骤2:添加命名空间
csharp
using Related_Log;
using Related_Log.Models;
步骤3:初始化(可选)
csharp
// 在程序启动时访问一次,确保单例初始化
var service = SystemConfigService.Instance;
// 可选:订阅日志事件用于实时显示
service.LogMessage += OnLogMessage;
service.XmlMessage += OnXmlMessage;
步骤4:开始使用
csharp
// INI配置
string ip = SystemConfigService.Instance.Config.Read("Network", "IP", "127.0.0.1");
// 文本日志
SystemConfigService.Instance.Log.Info("程序启动", "Program");
// XML日志
SystemConfigService.Instance.Xml.Log(button1, "Admin");
5.3 完整集成示例
示例1:简单控制台应用
csharp
using System;
using Related_Log;
namespace MyConsoleApp
{
class Program
{
static void Main(string[] args)
{
// 记录启动
SystemConfigService.Instance.Log.Info("程序启动", "Program");
try
{
// 读取配置
var config = SystemConfigService.Instance.Config;
string ip = config.Read("Server", "IP", "127.0.0.1");
int port = config.ReadInt("Server", "Port", 8080);
Console.WriteLine($"服务器地址: {ip}:{port}");
// 保存配置
config.Write("Server", "LastConnect", DateTime.Now.ToString());
// 记录日志
SystemConfigService.Instance.Log.Info($"连接到服务器 {ip}:{port}", "Program");
}
catch (Exception ex)
{
SystemConfigService.Instance.Log.Error("程序运行出错", ex, "Program");
}
// 清理旧日志
var result = SystemConfigService.Instance.CleanOldLogs(
LogRetentionDays.Days30,
LogRetentionDays.Days30
);
Console.WriteLine(result.GetSummary());
}
}
}
示例2:WinForms 应用
csharp
using System;
using System.Windows.Forms;
using Related_Log;
using Related_Log.Models;
namespace MyWinFormsApp
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
// 订阅日志事件,实时显示在界面上
SystemConfigService.Instance.LogMessage += OnLogMessage;
SystemConfigService.Instance.XmlMessage += OnXmlMessage;
}
private void OnLogMessage(object sender, LogEventArgs e)
{
// 跨线程更新UI
if (txtLog.InvokeRequired)
{
txtLog.Invoke(new Action(() => OnLogMessage(sender, e)));
return;
}
txtLog.AppendText($"[{e.Level}] {e.Message}\r\n");
}
private void OnXmlMessage(object sender, XmlLogEventArgs e)
{
if (txtXml.InvokeRequired)
{
txtXml.Invoke(new Action(() => OnXmlMessage(sender, e)));
return;
}
txtXml.AppendText($"[{e.Timestamp:HH:mm:ss}] {e.Username} - {e.Action}\r\n");
}
private void btnConnect_Click(object sender, EventArgs e)
{
try
{
// 记录操作
SystemConfigService.Instance.Xml.Log(sender, Environment.UserName);
// 读取配置
var config = SystemConfigService.Instance.Config;
string ip = config.Read("PLC", "IP", "192.168.1.1");
// 连接逻辑...
SystemConfigService.Instance.Log.Info($"PLC连接成功: {ip}", "MainForm");
}
catch (Exception ex)
{
SystemConfigService.Instance.Log.Error("PLC连接失败", ex, "MainForm");
}
}
private void btnClearLogs_Click(object sender, EventArgs e)
{
var result = SystemConfigService.Instance.CleanOldLogs(7, 30);
MessageBox.Show(result.GetSummary(), "清理结果");
SystemConfigService.Instance.Xml.Log(sender, Environment.UserName);
}
}
}
5.3 配置文件模板
创建默认的 config.ini 文件(可选):
ini
; ============================================
; 系统配置文件
; ============================================
[Network]
; 服务器IP地址
IP=127.0.0.1
; 服务器端口
Port=8080
[PLC]
; PLC通讯配置
IP=192.168.1.1
Port=502
SlaveId=1
; 是否启用PLC通讯
Activate=False
[Storage]
; 日志保留天数
Save_Log=30天
Save_Xml=30天
7. 常见问题FAQ
Q1: 日志写入会影响程序性能吗?
A: 不会。日志写入采用异步队列机制:
- 调用日志方法时,只是将日志放入内存队列(微秒级)
- 后台线程定期批量写入文件,不阻塞主线程
- 即使文件写入失败,也不会影响主程序运行
Q2: 程序意外关闭,队列中的日志会丢失吗?
A: 是的,队列中的日志会丢失。如果需要确保日志不丢失:
- 在程序退出时调用
SystemConfigService.Instance.Dispose()刷新队列 - 或在关键操作后调用强制刷新(需要扩展接口)
csharp
protected override void OnFormClosing(FormClosingEventArgs e)
{
SystemConfigService.Instance.Dispose(); // 刷新队列
base.OnFormClosing(e);
}
Q3: 如何在界面上实时显示日志?
A : 订阅 LogMessage 或 XmlMessage 事件:
csharp
SystemConfigService.Instance.LogMessage += (s, e) =>
{
// 使用 Invoke 确保线程安全
if (txtLog.InvokeRequired)
{
txtLog.Invoke(new Action(() => txtLog.AppendText(e.Message + "\r\n")));
}
else
{
txtLog.AppendText(e.Message + "\r\n");
}
};
Q4: INI文件读写失败怎么办?
A: 检查以下几点:
- 文件路径权限:确保程序对 INI 文件所在目录有读写权限
- 文件编码:确保 INI 文件使用 UTF-8 编码保存
- 调用异常 :
Read方法失败时会抛出异常,建议加 try-catch
csharp
try
{
string value = config.Read("Section", "Key", "default");
}
catch (Exception ex)
{
SystemConfigService.Instance.Log.Error("读取配置失败", ex, "Config");
}
Q5: 如何自定义日志文件存储路径?
A : 当前版本日志路径固定为应用程序目录下的 logs/ 和 logs_xml/。如需自定义,需要修改以下类:
LogFileService.cs:修改_logBaseDir初始化XmlLogService.cs:修改_logDirectory初始化LogCleanService.cs:修改对应的路径
Q6: 多个程序可以同时读写同一个 INI 文件吗?
A: 技术上可以,但不推荐。INI 文件没有内置的并发控制机制。
解决方案:
- 每个程序使用独立的 INI 文件
- 或使用数据库等支持并发的配置存储方式
Q7: 日志文件会无限增大吗?
A: 默认不会,需要手动调用清理方法。建议:
- 在程序启动时定期清理
- 或在配置页面提供清理按钮
csharp
// 程序启动时清理30天前的日志
SystemConfigService.Instance.CleanOldLogs(LogRetentionDays.Days30, LogRetentionDays.Days30);
8. 附录
7.1 命名空间汇总
| 命名空间 | 说明 |
|---|---|
Related_Log |
主命名空间,包含 SystemConfigService |
Related_Log.Models |
数据模型类(LogEntry、XmlLogEntry、事件参数等) |
Related_Log.Service.Related_Log |
文本日志服务(内部) |
Related_Log.Service.Related_Xml |
XML日志服务(内部) |
Related_Log.Service.Related_Ini |
INI配置服务(内部) |
Related_Log.Service.Related_Clean |
日志清理服务(内部) |
7.2 类型汇总
| 类型 | 命名空间 | 说明 |
|---|---|---|
SystemConfigService |
Related_Log |
主入口类(单例) |
LogService |
Related_Log |
文本日志封装类 |
XmlLogServiceWrapper |
Related_Log |
XML日志封装类 |
IniConfigService |
Related_Log.Service.Related_Ini |
INI配置服务 |
LogLevel |
Related_Log.Models |
日志级别枚举 |
LogRetentionDays |
Related_Log.Models |
日志保留天数枚举 |
LogEntry |
Related_Log.Models |
文本日志实体 |
XmlLogEntry |
Related_Log.Models |
XML日志实体 |
LogEventArgs |
Related_Log.Models |
日志事件参数 |
XmlLogEventArgs |
Related_Log.Models |
XML日志事件参数 |
LogCleanService.CleanResult |
Related_Log.Service.Related_Clean |
清理结果类 |
7.3 快速参考卡片
╔═══════════════════════════════════════════════════════════════╗
║ Related_Log 快速参考 ║
╠═══════════════════════════════════════════════════════════════╣
║ ║
║ 🔹 INI配置 ║
║ var config = SystemConfigService.Instance.Config; ║
║ config.Read(section, key, defaultValue) ║
║ config.Write(section, key, value) ║
║ config.ReadInt/ReadBool/ReadDouble... ║
║ ║
║ 🔹 文本日志 ║
║ SystemConfigService.Instance.Log.Info("消息", "来源") ║
║ .Warn("警告") ║
║ .Error("错误", exception) ║
║ .Debug("调试") ║
║ .Fatal("致命") ║
║ ║
║ 🔹 XML操作日志 ║
║ SystemConfigService.Instance.Xml.Log(sender, "用户名") ║
║ .Log("用户名", "操作", "详情") ║
║ ║
║ 🔹 日志清理 ║
║ SystemConfigService.Instance.CleanOldLogs( ║
║ LogRetentionDays.Days30, // 文本日志30天 ║
║ LogRetentionDays.Days60 // XML日志60天 ║
║ ) ║
║ ║
║ 🔹 事件订阅 ║
║ SystemConfigService.Instance.LogMessage += OnLog; ║
║ SystemConfigService.Instance.XmlMessage += OnXml; ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
7.4 版本历史
| 版本 | 日期 | 说明 |
|---|---|---|
| 1.0 | 2026-06-11 | 初始版本,包含INI、Log、XML三大核心功能 |