MessageQueue 和 Trace日志记录 windows 服务的应用
最近在做一个日志消息记录功能,比如某某管理员访问了啥,某某管理员操作了啥,某某编辑干了啥等等,类似于这种记录,一般来说这种记录可以用log4来做的,但我们的需求不一样
1,要满足细节上的操作比如某某管理员更新了篇文章,更新了这篇文章的啥东西如作者、标题、类别等清晰记录。
2,在记录入库时需要快速的响应并记录,可最大程度的优化代码,批处理sql
3,方便查询,某个时间段。某个功能点都能作为查询目标
综合考虑,我采用了新的一种方式处理并记录日志。
Trace 是.net后台跟踪监听代码执行的类,一般用来做调试跟踪bug用,让程序更方便的暴露出bug。在这里我用来跟踪代码并记录日志用
MessageQueue 大家都知道消息队列,用来存储信息通信用的,永久性的,只要消息没有从消息队列里读取,就会永远在那存在。不管当机还是什么情况都不会影响,除非自己手动清除它。
在MessageQueue里只有两个状态一个是往里扔,一个是往外取。所以我觉得用windows服务结合它来记录日志,最能保证各个重要的日志不会因为各种异常情况而没记录上的情况。
介绍下步骤,大致分为三步
一,Trace 记录
因为Trace 是一个监听类所以我们需要继承监听方法,并在webconfig里配置一下
using System; using System.Data.SqlClient; using System.Diagnostics; using System.IO; using Model; namespace MyLog { /// <summary> /// 日志监听操作类 /// </summary> public class ProjectTraceListener : TraceListener { public string FilePath { get; private set; } public ProjectTraceListener(string filepath) { FilePath = filepath; } /// <summary> /// 写入日志 /// </summary> /// <param name="message"></param> public override void Write(string message) { File.AppendAllText(FilePath, message); } public override void WriteLine(string message) { File.AppendAllText(FilePath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + message + Environment.NewLine); } /// <summary> /// 写入日志 /// </summary> /// <param name="o"></param> /// <param name="category"></param> public override void Write(object o, string category) { if (o == null) { WriteLine("异常写入:" + category); } else if (o is T_WorkingLog)//日志实体 { var entity = (T_WorkingLog)o; var connString = "server=.;uid=sa;password=admin_123456;database=TourismDB"; string cmdText = "INSERT INTO T_WorkingLog([AdminID],[EventType],[Action],[ActionIDs],[ActionContent],[ActionRemark],[IP],[URL],[CreateDate]) VALUES ({0},{1},{2},'{3}','{4}','{5}','{6}','{7}','{8}')"; var conn = new SqlConnection(connString); try { conn.Open(); var cmd = new SqlCommand(string.Format(cmdText,entity.AdminID,entity.EventType,entity.Action,entity.ActionIDs,entity.ActionContent,entity.ActionRemark,entity.IP,entity.URL,entity.CreateDate), conn); cmd.ExecuteNonQuery(); } catch (Exception ex) { WriteLine("异常写入:" + ex.Message); } finally { conn.Close(); } } else { WriteLine(o.ToString()); } } } }
我在这里只重写了write的三个方法其实还有很多都可以重写具体看应用,我暂时只用到写入记录操作
二,MessageQueue操作类的存放和读取
首页你得确认你得机器安装了MessageQueue,安装MQ的教程网上一搜一堆很简单,安装了MQ后会在计算机管理菜单里出现一个消息队列栏如下图
这代表着程序创建的消息队列名。可手动清除里面的消息
MQ的操作类里只需要三个方法 创建,接收,发送
using System; using System.Diagnostics; using System.Messaging; namespace MyLog { /// <summary> /// 日志类型的消息队列 /// </summary> public class LogQueue : IDisposable { protected MessageQueueTransactionType transactionType = MessageQueueTransactionType.Automatic; protected MessageQueue queue; protected TimeSpan timeout; //实现构造函数 public LogQueue(string queuePath, int timeoutSeconds) { Createqueue(queuePath); queue = new MessageQueue(queuePath); timeout = TimeSpan.FromSeconds(Convert.ToDouble(timeoutSeconds)); //设置当应用程序向消息对列发送消息时默认情况下使用的消息属性值 queue.DefaultPropertiesToSend.AttachSenderId = false; queue.DefaultPropertiesToSend.UseAuthentication = false; queue.DefaultPropertiesToSend.UseEncryption = false; queue.DefaultPropertiesToSend.AcknowledgeType = AcknowledgeTypes.None; queue.DefaultPropertiesToSend.UseJournalQueue = false; } /// <summary> /// 继承类将从自身的Receive方法中调用以下方法,该方法用于实现消息接收 /// </summary> public virtual object Receive() { try { using (Message message = queue.Receive(timeout, transactionType)) return message; } catch (MessageQueueException mqex) { if (mqex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) throw new TimeoutException(); throw; } } /// <summary> /// 继承类将从自身的Send方法中调用以下方法,该方法用于实现消息发送 /// </summary> public virtual void Send(object msg) { queue.Send(msg, transactionType); } /// <summary> /// 通过Create方法创建使用指定路径的新消息队列 /// </summary> /// <param name="queuePath"></param> public static void Createqueue(string queuePath) { try { if (!MessageQueue.Exists(queuePath)) { MessageQueue.Create(queuePath, true); //创建事务性的专用消息队列 Trace.WriteLine("创建队列成功!"); } } catch (MessageQueueException e) { Trace.WriteLine(e.Message); } } #region 实现 IDisposable 接口成员 public void Dispose() { queue.Dispose(); } #endregion } }
剩下的就是应用了。记录日志和读取日志操作
using System; using System.Messaging; using Model; namespace MyLog { /// <summary> /// 日志任务 /// </summary> public class LogJob : LogQueue { // 获取配置文件中有关消息队列路径的参数 private static readonly string queuePath = @".\Private$\PSLogManager"; private static int queueTimeout = 20; /// <summary> /// 实现日志任务构造函数 /// </summary> public LogJob() : base(queuePath, queueTimeout) { // 设置消息的序列化采用二进制方式 queue.Formatter = new BinaryMessageFormatter(); } /// <summary> /// 调用PetShopQueue基类方法,实现从消息队列中接收日志消息 /// </summary> /// <returns>订单对象 OrderInfo</returns> public new T_WorkingLog Receive() { // 指定消息队列事务的类型,Automatic枚举值允许发送发部事务和从外部事务接收 transactionType = MessageQueueTransactionType.Automatic; return (T_WorkingLog)((Message)base.Receive()).Body; } /// <summary> /// 该方法从日志队列里接收日志消息 /// </summary> /// <param name="timeOut"></param> /// <returns></returns> public T_WorkingLog Receive(int timeOut) { timeout = TimeSpan.FromSeconds(Convert.ToDouble(timeOut)); return Receive(); } /// <summary> /// 调用PetShopQueue基类方法,实现从消息队列中发送日志消息 /// </summary> /// <param name="logMessage">日志对象 T_WorkingLog</param> public void Send(T_WorkingLog logMessage) { // 指定消息队列事务的类型,Single枚举值用于单个内部事务的事务类型 transactionType = MessageQueueTransactionType.Single; base.Send(logMessage); } } }
到了这里我们就有了写日志和收日志的功能,但我们还需要最后一步windows服务来结合。
三,windows服务的开发和部署
新建一个windows服务,我们需要一个执行类,设置执行时间,没隔多少分钟跑一次mq的消息来记录入库
using System; using System.Collections; using System.Diagnostics; using System.Threading; using System.Transactions; using Model; using MyLog; namespace LoggingService { public class ExcuteClass { private static double transactionTimeout = 6; //一个任务预估执行时间,以秒为单位(这是预估一条数据入库的执行时间) private static int queueTimeout = 20; //任务响应时间,以秒为单位,每隔20秒(节省资源可加长响应时间) private static int batchSize = 100; //一个线程处理的任务数量 private static int threadCount = 1; //分配线程数(注:不是越多越快,根据数据量来预估执行线程) // 定义一个静态变量来保存类的实例 private static ExcuteClass uniqueInstance; // 定义一个标识确保线程同步 private static readonly object locker = new object(); /// <summary> /// 单列方法 /// </summary> /// <returns></returns> public static ExcuteClass GetInstance() { // 当第一个线程运行到这里时,此时会对locker对象 "加锁", // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁 // lock语句运行完之后(即线程运行完之后)会对该对象"解锁" lock (locker) { // 如果类的实例不存在则创建,否则直接返回 if (uniqueInstance == null) { uniqueInstance = new ExcuteClass(); } } return uniqueInstance; } /// <summary> /// 启动接收任务线程 /// </summary> public void CustomerStart() { //声明线程 Thread workTicketThread; Thread[] workerThreads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { //创建 Thread 实例 workTicketThread = new Thread(ProcessLog) { IsBackground = true }; // 设置线程在后台工作和线程启动前的单元状态(STA表示将创建并进入一个单线程单元 ) workTicketThread.SetApartmentState(ApartmentState.STA); //启动线程,将调用ThreadStart委托 workTicketThread.Start(); workerThreads[i] = workTicketThread; } using (System.IO.StreamWriter sw = new System.IO.StreamWriter(@"C:\\log.txt", true)) { sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + "进程已经开始启动......"); } } private static void ProcessLog() { // 总事务处理时间(tsTimeout )就该超过批处理任务消息的总时间 TimeSpan tsTimeout = TimeSpan.FromSeconds(Convert.ToDouble(transactionTimeout * batchSize)); LogJob logJob = new LogJob(); while (true) { // 消息队列花费时间 TimeSpan datetimeStarting = new TimeSpan(DateTime.Now.Ticks); double elapsedTime = 0; int processedItems = 0; ArrayList queueLog = new ArrayList(); using (System.IO.StreamWriter sw = new System.IO.StreamWriter(@"C:\\log.txt", true)) { sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + "批处理日志写入开始......"); } try { using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required, tsTimeout)) { // 接收来自消息队列的任务消息 for (int j = 0; j < batchSize; j++) { try { //如果有足够的时间,那么接收任务,并将任务存储在数组中 if ((elapsedTime + queueTimeout + transactionTimeout) < tsTimeout.TotalSeconds) { queueLog.Add(logJob.Receive(queueTimeout)); } else { j = batchSize; // 结束循环 } //更新已占用时间 elapsedTime = new TimeSpan(DateTime.Now.Ticks).TotalSeconds - datetimeStarting.TotalSeconds; } catch (TimeoutException) { //结束循环因为没有可等待的任务消息 j = batchSize; } } //从数组中循环取出任务对象,并将任务插入到数据库中 for (int k = 0; k < queueLog.Count; k++) { ExcuteClass sh = new ExcuteClass(); sh.IndexOn((T_WorkingLog)queueLog[k]); processedItems++; } //指示范围中的所有操作都已成功完成 ts.Complete(); } } catch (Exception ex) { using (System.IO.StreamWriter sw = new System.IO.StreamWriter(@"C:\\log.txt", true)) { sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + ex.Message); } } //完成后显示处理信息 using (System.IO.StreamWriter sw = new System.IO.StreamWriter(@"C:\\log.txt", true)) { sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + "(线程 Id " + Thread.CurrentThread.ManagedThreadId + ") 批处理完成, " + processedItems + " 任务, 处理花费时间: " + elapsedTime + " 秒."); } } } /// <summary> /// 写入任务线程 /// </summary> private void IndexOn(T_WorkingLog job) { Trace.Write(job, ""); } } }
这方法写的很明确,利用多线程去循环消息队列里的消息,然后取出消息入库。
然后部署发布这个服务。这个阶段再单写一篇,这篇不够写遇到的问题比较多。