在我们做项目的过程中,日志跟踪异常是非常必要的,当程序发布到服务器上时,如果出现异常直接抛出给用户这是非常不友好的。对于不懂程序的用户来说,这回让人感觉莫名其妙,对于那些程序高手,可能就是攻破这个网站的关键。
在asp.net 程序的web.config 配置文件中有如下两个节点作为程序异常配置:
(1)<customErrors>节点
<customErrors>节点用于定义 一些自定义错误信息的信息。此节点有Mode和defaultRedirect两个属性,其中 defaultRedirect属性是一个可选属性,表示应 用程序发生错误时重定向到的默认URL,如果没有指定该属性则显示一般性错误。 Mode属性是一个必选属性,它有三个可能值,它们所代表的意义分别如下:
On 表示在本地和远程用户都会看到自定义错误信息。
Off 禁用自定义错误信息,本地和远程用户都会看到详细的错误信息。
RemoteOnly 表示本地用户将看到详细错误信息,而远程用户将会看到自定义错误信息。
这 里有必要说明一下本地用户和远程用户的概念。当我们访问asp.net应用程时所使用的机器和发布asp.net应用程序所使用的机器为同一台机器时成为 本地用户,反之则称之为远程用户。在开发调试阶段为了便于查找错误Mode属性建议设置为Off,而在部署阶段应将Mode属性设置为On或者 RemoteOnly,以避免这些详细的错误信息暴露了程序代码细节从而引来黑客的入侵。
(2) <error>子节点
在<customErrors>节点下还包含有< error>子节点,这个节点主要是根据服务器的HTTP错误状态代码而重定向到我们自定义的错误页面,注意要使<error>子节点 下的配置生效,必须将<customErrors>节点节点的Mode属性设置为“On”。下面是一个例子:
<customErrors mode="On" defaultRedirect="GenericErrorPage.htm">
<error statusCode="403" redirect="403.htm" />
<error statusCode="404" redirect="404.htm" />
</customErrors>
以上配置可以让程序方式异常的时候显示更友好,不将异常信息直接抛出给用户。这个时候我们就需要达到用户友好提示的同时还要跟踪程序异常。下面介绍的是ORM框架中提供的一个小小的日志跟踪程序。
1. 异常消息类型
代码
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace CommonData.Log
7 {
8 public enum MessageType
9 {
10 /// <summary>
11 /// 未知信息异常
12 /// </summary>
13 Unkown,
14
15 /// <summary>
16 ///普通信息异常
17 /// </summary>
18 Common,
19
20 /// <summary>
21 /// 警告信息异常
22 /// </summary>
23 Warning,
24
25 /// <summary>
26 /// 错误信息异常
27 /// </summary>
28 Error,
29
30 /// <summary>
31 /// 成功信息
32 /// </summary>
33 Success
34 }
35 }
36
以上定义的是一个枚举类型,定义了日志的级别,分别为:未知信息异常, 普通信息异常 , 警告信息异常, 错误信息异常, 成功信息.
写程序的过程中可以根据不同的需要输出不同级别的日志。
(2). 日志文件创建类别
代码
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace CommonData.Log
7 {
8 public enum LogType
9 {
10 /// <summary>
11 /// 此枚举指示每天创建一个新的日志文件
12 /// </summary>
13 Daily,
14
15 /// <summary>
16 /// 此枚举指示每周创建一个新的日志文件
17 /// </summary>
18 Weekly,
19
20 /// <summary>
21 /// 此枚举指示每月创建一个新的日志文件
22 /// </summary>
23 Monthly,
24
25 /// <summary>
26 /// 此枚举指示每年创建一个新的日志文件
27 /// </summary>
28 Annually
29 }
30 }
31
这也是一个枚举类型,定义了日志文件创建的标准。程序可以每天创建一个日志文件,可以一周,一月,一年来创建一次。一般情况写我是使用每天创建一个日志文件,这样可以每日下载该日志文件跟踪程序异常。如果我们每周或更长时间创建一次,这样在这个是时段内就不能即时清理改日志文件(因为该文件在当前线程中使用,后面介绍)。
(3).定义日志消息实体类
代码
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace CommonData.Log
7 {
8 public class LogMessage
9 {
10 /// <summary>
11 /// 日志记录时间
12 /// </summary>
13 private DateTime _time;
14
15 public DateTime Time
16 {
17 get
18 {
19 return _time;
20 }
21 set
22 {
23 _time = value;
24 }
25 }
26
27 /// <summary>
28 /// 日志记录内容
29 /// </summary>
30 private string _content;
31
32 public string Content
33 {
34 get
35 {
36 return _content;
37 }
38 set
39 {
40 _content = value;
41 }
42 }
43
44 /// <summary>
45 /// 日志类型
46 /// </summary>
47 private MessageType _type;
48
49 public MessageType Type
50 {
51 get
52 {
53 return _type;
54 }
55 set
56 {
57 _type = value;
58 }
59 }
60
61
62 public LogMessage(): this("", MessageType.Unkown)
63 {
64 }
65
66 public LogMessage(string content, MessageType type): this(DateTime.Now, content, type)
67 {
68 }
69
70 public LogMessage(DateTime time,string content,MessageType type)
71 {
72 this._time = time;
73 this._content = content;
74 this._type = type;
75 }
76
77 public override string ToString()
78 {
79 return this._time.ToString() + "\t" + this._content + "\t";
80 }
81 }
82 }
83
这个类定义了日志消息的一些基本属性,其中很重要的一点就是 重写ToString 这个方法。该对象具有了新ToString方法,它返回的是该消息的时间和类容。这样在后面的日志记录和输出过程中比较方便,而不用每次都去连接字符串。
(4) 日志记录
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
namespace CommonData.Log
{
public class Log:IDisposable
{
private static Queue<LogMessage> logMessages;
private static string path;
private static bool state;
private static LogType logtype;
private static DateTime time;
private static StreamWriter writer;
/// <summary>
/// 无参构造方法,指定日志文件路径为当前目录,默认日志类型为每日日志类型
/// </summary>
public Log():this(".\\",LogType.Daily)
{
}
/// <summary>
/// 构造方法,指定日志文件路径为当前目录
/// </summary>
/// <param name="t">日志文件类型</param>
public Log(LogType t):this(".\\",t)
{
}
/// <summary>
/// 构造方法
/// </summary>
/// <param name="path">指定日志文件路径</param>
/// <param name="t">日志文件类型</param>
public Log(string filepath,LogType t)
{
if(logMessages==null)
{
state = true;
path= filepath;
logtype = t;
FileOpen();
logMessages = new Queue<LogMessage>();
Thread thread = new Thread(Work);
thread.Start();
}
}
/// <summary>
/// 利用线程来写入日志内容
/// </summary>
private void Work()
{
while(true)
{
if(logMessages.Count>0)
{
LogMessage message = null;
lock (logMessages)
{
message = logMessages.Dequeue();
}
if(message!=null)
{
WriteLogMessage(message);
}
}
else
{
if(state)
{
Thread.Sleep(1);
}
else
{
FileClose();
}
}
}
}
/// <summary>
/// 将日志内容写入到文本
/// </summary>
private void WriteLogMessage(LogMessage message)
{
try
{
if (writer == null)
{
FileOpen();
}
else
{
if (DateTime.Now >= time)
{
FileClose();
FileOpen();
}
writer.Write(message.Time);
writer.Write("\t");
writer.Write(message.Type);
writer.Write("\t\r\n");
writer.Write(message.Content);
writer.Write("\r\n\r\n");
writer.Flush();
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
/// <summary>
/// 根据日志文件类型判断日志文件名称
/// 通过判断文件的到期时间标记将决定是否创建新文件。
/// </summary>
/// <returns></returns>
private string GetFileName()
{
DateTime now = DateTime.Now;
string format = "";
switch (logtype)
{
case LogType.Daily:
time = new DateTime(now.Year,now.Month,now.Day);
time = time.AddDays(1);
//time = time.AddMinutes(1);
format = "yyyyMMdd'.log'";
break;
case LogType.Weekly:
time = new DateTime(now.Year,now.Month,now.Day);
time = time.AddDays(7);
format = "yyyyMMdd'.log'";
break;
case LogType.Monthly:
time = new DateTime(now.Year,now.Month,1);
time = time.AddMonths(1);
format = "yyyyMM'.log'";
break;
case LogType.Annually:
time = new DateTime(now.Year,1,1);
time = time.AddYears(1);
format = "yyyy'.log'";
break;
}
return now.ToString(format);
}
/// <summary>
/// 写入日志文件
/// </summary>
public void Write(LogMessage message)
{
if (logMessages != null)
{
lock (logMessages)
{
logMessages.Enqueue(message);
}
}
}
/// <summary>
/// 写入日志文件
/// </summary>
/// <param name="text">日志内容</param>
/// <param name="type">消息类型</param>
public void Write(string text,MessageType type)
{
Write(new LogMessage(text,type));
}
/// <summary>
/// 写入日志文件
/// </summary>
/// <param name="now">当前时间</param>
/// <param name="text">日志内容</param>
/// <param name="type">消息类型</param>
public void Write(DateTime now, string text, MessageType type)
{
Write(new LogMessage(now,text,type));
}
/// <summary>
/// 写入日志文件
/// </summary>
/// <param name="e">异常对象</param>
/// <param name="type">消息类型</param>
public void Write(Exception e,MessageType type)
{
Write(new LogMessage(e.Message,type));
}
/// <summary>
/// 打开文件准备写入内容
/// </summary>
private void FileOpen()
{
writer = new StreamWriter(path+GetFileName(),true,Encoding.Default);
}
/// <summary>
/// 关闭文件流
/// </summary>
private void FileClose()
{
if(writer!=null)
{
writer.Flush();
writer.Close();
writer.Dispose();
writer = null;
}
}
/// <summary>
/// 释放内存空间
/// </summary>
public void Dispose()
{
state = false;
GC.SuppressFinalize(this);
}
}
}
这个才是这个日志系统的关键部分,程序设计的思路是程序启动之后,程序开辟了另外一条线程,该线程专用于写日志信息。程序中使用了一个队列消息,当程序发生异常或者用户记录日志,会将这个日志消息放入这个队列中去,至于写日志主程序就不用再去处理,只要捕获到异常主程序只是负责将消息放入队列就可以了。写日志的工作是有开辟的线程完成的。该线程会根据定义在多长时间内创建新的日志文件,还会不断的去队列中检测是否有新的日志内容需要记录,并且完成日志的记录。正是因为这个线程,所以在日志记录的过程中不能去清理这个日志文件。所以要特别注意。