编码笔记

导航

写一个日志类用于跟踪调试

根据自己的工作需要,借鉴了网上一些分析和尝试,自己写了一个日志的单元用于服务器的跟踪调试。

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;

 

单元文件

posted on 2014-07-15 11:25 封三郎 阅读(...) 评论(...) 编辑 收藏