写一个日志类用于跟踪调试
根据自己的工作需要,借鉴了网上一些分析和尝试,自己写了一个日志的单元用于服务器的跟踪调试。
unit LogUnit; interface uses System.Classes,System.SysUtils,System.Generics.Collections,Windows,Forms,IOUtils, Vcl.StdCtrls,Winapi.Messages; const FLF = #13#10; // 换行符 type /// <summary>负责写日志的线程类</summary> TWriteLogToFileThread = class(TThread) private FCSLock: TRTLCriticalSection; //临界区 FLogFileSteam : TFileStream; FLogType : string; FUIControl : TComponent; FLogBuff,FBuffA,FBuffB:TMemoryStream; FBegCount:DWord; // 开始写文件的时间 function getLogFileName: string; protected procedure WriteToFile(); procedure Execute();override; procedure WriteLog(const InBuff:Pointer;InSize:Integer);overload; public constructor Create(ALogType : string; AUIControl : TComponent=nil); destructor Destroy();override; procedure WriteLog(const Msg:string);overload; property FileName:string read getLogFileName; end; /// <summary>日志管理基类</summary> TLogClass = class private FWriteLogThreadList : TWriteLogToFileThread; protected /// <summary>日志类型</summary> /// <remarks>纯虚函数,子类覆写该函数</remarks> function GetLogType : string; virtual; abstract; public /// <summary>构造函数</summary> /// <param name="ALogType">日志类型,用于确定日志文件的文件名。</param> /// <param name="AUIControl">显示日志的可视化控件。</param> constructor Create(AUIControl : TComponent=nil); virtual; destructor Destroy; override; /// <summary>记录日志</summary> procedure WriteLog(const Msg:string); function LogFileName : string; end; /// <summary>服务器端通用日志类</summary> TServerLog = class(TLogClass) protected /// <summary>覆盖父类的虚函数,输出日志类型描述。</summary> function GetLogType : string; override; end; /// <summary>服务器端内核工作日志类</summary> TServerCoreLog = class(TLogClass) protected /// <summary>覆盖父类的虚函数,输出日志类型描述。</summary> function GetLogType : string; override; end; /// <summary>日志类管理类,包装了一组日志类。</summary> TLogManager = class public class function ServerLog(AUIControl : TComponent=nil) : TServerLog; class function ServerCoreLog(AUIControl : TComponent=nil) : TServerCoreLog; end; implementation type /// <summary>负责创建日志路径的管理类</summary> /// <remarks>这个类是全局唯一的</remarks> TLogFilePathClass = class private FCSLock: TRTLCriticalSection; //临界区 public constructor Create; destructor Destroy; override; class function LogFilePathObject: TLogFilePathClass; function LogFilePath() : string; end; var gInnerLogFilePathThread : TLogFilePathClass; gInnerServerLog : TServerLog; gInnerServerCoreLog : TServerCoreLog; { TLogClass } constructor TLogClass.Create(AUIControl: TComponent); begin FWriteLogThreadList := TWriteLogToFileThread.Create(GetLogType, AUIControl); end; destructor TLogClass.Destroy; begin FWriteLogThreadList.Terminate; inherited; end; function TLogClass.LogFileName: string; begin Result := FWriteLogThreadList.FileName; end; procedure TLogClass.WriteLog(const Msg: string); begin FWriteLogThreadList.WriteLog(Msg); end; { TLogFilePathClass } constructor TLogFilePathClass.Create; begin InitializeCriticalSection(FCSLock); end; destructor TLogFilePathClass.Destroy; begin DeleteCriticalSection(FCSLock); inherited; end; function TLogFilePathClass.LogFilePath(): string; var szPath : string; begin EnterCriticalSection(FCSLock); try // szPath := Application.ExeName szPath := TDirectory.GetCurrentDirectory; szPath := szPath+'\log\'+FormatDateTime('yyyy-MM-dd', Now)+'\'; if not TDirectory.Exists(szPath) then TDirectory.CreateDirectory(szPath); Exit(szPath); finally LeaveCriticalSection(FCSLock); end; end; class function TLogFilePathClass.LogFilePathObject: TLogFilePathClass; begin if gInnerLogFilePathThread = nil then gInnerLogFilePathThread := TLogFilePathClass.Create; Exit(gInnerLogFilePathThread); end; { TWriteLogToFileThread } constructor TWriteLogToFileThread.Create(ALogType: string; AUIControl: TComponent); var LogFileName : string; begin if Trim(ALogType) = '' then raise exception.Create('ALogType not ""'); inherited Create(TRUE); Self.FreeOnTerminate := True; InitializeCriticalSection(FCSLock); FLogType := ALogType; FUIControl := AUIControl; //队列缓冲区A,B运行的时候,交替使用 Self.FBuffA := TMemoryStream.Create(); Self.FBuffA.Size := 1024 * 1024; //初始值可以根据需要自行调整 Self.FBuffB := TMemoryStream.Create(); Self.FBuffB.Size := 1024 * 1024; //初始值可以根据需要自行调整 Self.FLogBuff := Self.FBuffA; LogFileName := getLogFileName; if FileExists(LogfileName) then begin FLogFileSteam := TFileStream.Create(LogFileName,fmOpenWrite or fmShareDenyWrite); FLogFileSteam.Position := FLogFileSteam.Size; //如果文件已经存在,数据进行追加 end else FLogFileSteam := TFileStream.Create(LogFileName,fmCreate or fmShareDenyWrite); //启动执行 Self.Resume(); end; destructor TWriteLogToFileThread.Destroy; begin FBuffA.Free(); FBuffB.Free(); FLogFileSteam.Free(); DeleteCriticalSection(FCSLock); inherited; end; procedure TWriteLogToFileThread.Execute; begin inherited; FBegCount := GetTickCount(); while(not Self.Terminated) do begin // 数据写入磁盘的间隔,即每2秒写一次日志文件。 if (GetTickCount() - FBegCount) >= 2000 then begin WriteToFile(); FBegCount := GetTickCount(); end else Sleep(200); end; WriteToFile(); end; function TWriteLogToFileThread.getLogFileName: string; begin Exit(TLogFilePathClass.LogFilePathObject.LogFilePath()+FLogType+'.LOG'); end; procedure TWriteLogToFileThread.WriteLog(const InBuff: Pointer; InSize: Integer); var TmpStr:string; Bytes : TBytes; lineCount: Integer; begin TmpStr := FormatDateTime('YYYY-MM-DD hh:mm:ss zzz ',Now()); EnterCriticalSection(FCSLock); try Bytes := TEnCoding.UTF8.GetBytes(TmpStr); FLogBuff.Write(Bytes, Length(Bytes)); TmpStr := string(InBuff); Bytes := TEnCoding.UTF8.GetBytes(InBuff); FLogBuff.Write(Bytes, Length(Bytes)); Bytes := TEnCoding.UTF8.GetBytes(FLF); FLogBuff.Write(Bytes, Length(Bytes)); if FUIControl <> nil then begin if FUIControl is TMemo then begin lineCount := TMemo(FUIControl).Lines.Add(string(InBuff)); //滚屏到最后一行 SendMessage(TMemo(FUIControl).Handle,WM_VSCROLL,SB_LINEDOWN,0); // 设定一个最大显示行数,如果超过这个行数,就清除。 if lineCount >= 3 then TMemo(FUIControl).Clear; end; end; finally LeaveCriticalSection(FCSLock); end; end; procedure TWriteLogToFileThread.WriteLog(const Msg: string); begin WriteLog(Pointer(Msg),Length(Msg)); end; procedure TWriteLogToFileThread.WriteToFile; var MS:TMemoryStream; LogFileName : string; begin EnterCriticalSection(FCSLock); //交换缓冲区 try MS := nil; if FLogBuff.Position > 0 then begin MS := FLogBuff; if FLogBuff = FBuffA then FLogBuff := FBuffB else FLogBuff := FBuffA; FLogBuff.Position := 0; end; finally LeaveCriticalSection(FCSLock); end; if MS = nil then Exit; //写入文件 try LogFileName := getLogFileName; // 如果日志文件名(这里实质是路径)发生改变,则需要重新创建流。 // 路径的改变只有一个原因:日期发生了改变。 if FLogFileSteam.FileName <> LogFileName then begin if FileExists(LogfileName) then begin FLogFileSteam := TFileStream.Create(LogFileName,fmOpenWrite or fmShareDenyWrite); FLogFileSteam.Position := FLogFileSteam.Size; //如果文件已经存在,数据进行追加 end else FLogFileSteam := TFileStream.Create(LogFileName,fmCreate or fmShareDenyWrite); end else // 否则的话直接写流。 begin FLogFileSteam.Write(MS.Memory^,MS.Position); end; finally MS.Position := 0; end; end; { TServerLog } function TServerLog.GetLogType: string; begin Result := 'ServerLog'; end; { TLogManager } class function TLogManager.ServerCoreLog( AUIControl: TComponent): TServerCoreLog; begin if gInnerServerCoreLog = nil then gInnerServerCoreLog := TServerCoreLog.Create(AUIControl); Exit(gInnerServerCoreLog); end; class function TLogManager.ServerLog(AUIControl: TComponent): TServerLog; begin if gInnerServerLog = nil then gInnerServerLog := TServerLog.Create(AUIControl); Exit(gInnerServerLog); end; { TServerCoreLog } function TServerCoreLog.GetLogType: string; begin Result := 'ServerCoreLog'; end; initialization finalization if gInnerServerLog <> nil then gInnerServerLog.Free; if gInnerServerCoreLog <> nil then gInnerServerCoreLog.Free; if gInnerLogFilePathThread <> nil then gInnerLogFilePathThread.Free; end.
写这个单元的时候,主要考虑了几个方面:
1.性能,不能卡程序;
此点通过多线程、流缓冲来解决。
2.接口调用方便;
此点通过TLogManager类封装日志对象输出调用来解决。
3.易于扩展出不同的日志类;
此点通过剥离TLogClass和TWriteLogToFileThread来实现,TWriteLogToFileThread负责流和文件的读写。TLogClass负责服务接口的提供。需要不同的日志类时,只需要继承TLogClass类,而不需要重复去处理流和多线程。
4.日志文件组织结构清晰
通过日志的类型和日期来组织日志文件的结构。
这里遇到过一个麻烦,就是XE6写流的时候,如果采取以前的老方式来写的话,字符串只会截取到1/2的内容。
老的方式如下:
procedure TWriteLogToFileThread.WriteLog(const InBuff: Pointer; InSize: Integer);
var
TmpStr:string;
begin
// ...
TmpStr := FormatDateTime('YYYY-MM-DD hh:mm:ss zzz ',Now()); FLogBuff.Write(TmpStr[1],Length(TmpStr)); FLogBuff.Write(InBuff^,InSize); FLogBuff.Write(FLF[1],2); // ... end;
原因可能是Delphi的字符串流的格式还是ansi的?
通过TEnCoding类把string转成TBytes以后,再写入流就OK了。
代码如下:
var TmpStr:string; Bytes : TBytes; begin // ... Bytes := TEnCoding.UTF8.GetBytes(TmpStr); FLogBuff.Write(Bytes, Length(Bytes)); // ... end;