1

我有一个服务应用程序,我将很快实现一个日志文件。在开始编写它如何保存日志文件之前,我还有一个要求,即应该可以使用一个小的简单表单应用程序来实时查看日志。换句话说,如果服务将某些内容写入日志,它不仅应该将其保存到文件中,而且其他应用程序应该立即知道并显示记录的内容。

一个肮脏的解决方案是让这个应用程序不断地打开这个文件并检查最近的变化,并加载任何新的东西。但这是非常草率和沉重的。另一方面,我可以编写一个服务器/客户端套接字对并通过那里监视它,但我认为使用 TCP/IP 发送一个字符串有点过载。我正在考虑使用文件方法,但是我该如何以一种不会那么重的方式来实现呢?换句话说,假设日志文件增长到 100 万行。我不想加载整个文件,我只需要检查文件末尾是否有新数据。我也可以延迟最多 5 秒,但这与“实时”相矛盾。

我熟悉的读取/写入文件的唯一方法包括保持文件打开/锁定和读取文件的所有内容,我不知道如何只从文件末尾读取部分内容并保护它从试图访问它的两个应用程序。

4

6 回答 6

4

你所要求的正是我在我公司的一个项目中所做的。

它有一个托管进程外 COM 对象的服务,因此我们所有的应用程序都可以将消息写入中央日志文件,然后是一个单独的查看器应用程序,该应用程序使用相同的 COM 对象在日志出现时直接从服务接收通知文件更改。COM 对象让查看者知道日志文件的物理位置,以便查看者可以在需要时直接打开文件。

对于收到的每个通知,查看器都会检查新文件的大小,然后仅读取自上次通知以来写入的新字节(查看器会跟踪先前的文件大小)。在早期版本中,我让服务直接将每个单独的日志条目推送到查看器,但在负载过重的情况下需要筛选大量流量,所以我最终取消了该功能并让查看器处理读取数据相反,它可以更有效地一次读取多个日志条目。

服务和查看器都同时打开了日志文件。当服务创建/打开日志文件时,它将文件设置为具有只读共享的读/写访问权限。当查看器打开文件时,它会将文件设置为具有读/写共享的只读访问权限(因此服务仍然可以对其进行写入)。

不用说,服务和查看器都在同一台机器上运行,因此它们可以访问同一个本地文件(不使用远程文件)。尽管该服务确实具有通过 TCP/IP 将日志条目转发到在另一台机器上运行的服务的远程实例的功能(然后在该机器上运行的查看器可以看到它们)。

于 2012-08-29T23:49:02.827 回答
3

我们的开源TSynLog类满足您的大部分需求- 它已经稳定且经过验证(用于现实世界的应用程序,包括服务)。

它主要具有快速日志记录(具有一组级别,而不是级别层次结构)、带有堆栈跟踪的异常拦截和自定义日志记录(包括将对象序列化为日志中的 JSON)。

您甚至还有一些附加功能,例如客户端方法分析器日志查看器

日志文件在生成期间被锁定:您可以读取它们,而不是修改它们。

适用于从 Delphi 5 到 XE2,完全开源并每日更新。

于 2012-08-30T05:40:20.450 回答
2

这听起来像是一个完全疯狂的答案,但是..

我使用 Gurock Softwares Smart Inspect.. http://www.gurock.com/smartinspect/ 它很棒,因为您可以发送图片、变量以及所有内容,因此当您需要文本自动取款机时,它非常适合观看您的应用程序即使在远程机器上也是实时的..它可以将其发送到本地文件..

它可能是对您的问题的有用答案,或者是一条红鲱鱼 - 它有点不合常规,但您以后可能会觉得值得加入它的附加功能(例如,如果出现严重错误,它非常适合捕获信息)

于 2012-08-29T22:06:49.813 回答
2

几年前,我编写了一个循环缓冲区二进制文件跟踪日志系统,它避免了文件无限增长的问题,同时为我提供了我想要的功能,例如,如果我愿意,能够看到问题,否则,能够忽略跟踪缓冲区。

但是,如果您想要一个连续的在线系统,那么我根本不会使用文件。

我使用文件是因为我确实想要类似文件的持久性并且不需要运行侦听器应用程序。我只是想要文件解决方案,因为我希望无论现在是否有人在“听”,我都希望记录发生,但没有使用无休止增长的文本日志,因为我担心在日志文件上用完数百兆,并填满我 250 兆字节的硬盘。在 1 TB 硬盘的时代,人们几乎没有这样的担忧。

正如大卫所说,客户端服务器解决方案是最好的,而且并不复杂。

但是你可能更喜欢文件,就像我一样,在我的情况下。我只是将我的查看器应用程序作为崩溃后运行的事后分析工具启动。这是在 MadExcept 或类似的东西出现之前,所以我有一些应用程序刚刚死掉,我想知道发生了什么。

在我的循环缓冲区之前,我会使用像 sys-internals DebugView 和 OutputDebugString 这样的调试视图工具,但是在我启动 DebugView 之前发生崩溃时,这对我没有帮助。

基于文件的日志记录(二进制)是我允许自己创建二进制文件的少数几次之一。我通常讨厌讨厌讨厌二进制文件。但是您只是尝试制作一个循环缓冲区而不使用固定长度的二进制记录。

这是一个示例单元。如果我现在而不是在 1997 年写这篇文章,我就不会使用“记录文件”,但是,嘿,就是这样。

要扩展此单元以便将其用作实时查看器,我建议您只需检查二进制文件上的日期时间戳并每 1-5 秒刷新一次(您的选择),但仅当二进制跟踪上的日期时间戳文件已更改。不难,也不会对系统造成很大的负担。

该单元用于记录器和查看器,它是一个可以读取和写入磁盘上的循环缓冲区二进制文件的类。

unit trace;

{$Q-}
{$I-}

interface

uses Classes;

const
  traceBinMsgLength = 255; // binary record message length
  traceEOFMARKER = $FFFFFFFF;

type
  TTraceRec = record
    index: Cardinal;
    tickcount: Cardinal;
    msg: array[0..traceBinMsgLength] of AnsiChar;
  end;
  PTraceBinRecord = ^TTraceRec;
  TTraceFileOfRecord = file of TTraceRec;

  TTraceBinFile = class
    FFilename: string;
    FFileMode: Integer;
    FTraceFileInfo: string;
    FStorageSize: Integer;
    FLastIndex: Integer;
    FHeaderRec: TTraceRec;
    FFileRec: TTraceRec;
    FAutoIncrementValue: Cardinal;
    FBinaryFileOpen: Boolean;
    FBinaryFile: TTraceFileOfRecord;
    FAddTraceMessageWhenClosing: Boolean;
  public
    procedure InitializeFile;
    procedure CloseFile;
    procedure Trace(msg: string);
    procedure OpenFile;
    procedure LoadTrace(traceStrs: TStrings);
    constructor Create;
    destructor Destroy; override;

    property Filename: string       read FFilename write FFilename;
    property TraceFileInfo: string  read FTraceFileInfo write FTraceFileInfo;

   // Default 1000 rows.
   // change storageSize to the size you want your circular file to be before
   // you create and write it. Remember to set the value to the same number before
   // trying to read it back, or you'll have trouble.
    property StorageSize: Integer   read FStorageSize write FStorageSize;
    property AddTraceMessageWhenClosing: Boolean
      read FAddTraceMessageWhenClosing write FAddTraceMessageWhenClosing;

  end;

implementation

uses SysUtils;

procedure SetMsg(pRec: PTraceBinRecord; msg: ansistring);
var
  n: Integer;
begin
  n := length(msg);
  if (n >= traceBinMsgLength) then
  begin
    msg := Copy(msg, 1, traceBinMsgLength);
    n := traceBinMsgLength;
  end;
  StrCopy({Dest} pRec^.msg, {Source} PAnsiChar(msg));
  pRec^.msg[n] := Chr(0); // ensure nul char termination
end;

function IsBlank(var aRec: TTraceRec): Boolean;
begin
  Result := (aRec.msg[0] = Chr(0));
end;

procedure TTraceBinFile.CloseFile;
begin
  if FBinaryFileOpen then
  begin
    if FAddTraceMessageWhenClosing then
    begin
      Trace('*END*');
    end;
    System.CloseFile(FBinaryFile);
    FBinaryFileOpen := False;
  end;
end;

constructor TTraceBinFile.Create;
begin
  FLastIndex := 0; // lastIndex=0 means blank file.
  FStorageSize := 1000; // default.
end;

destructor TTraceBinFile.Destroy;
begin
  CloseFile;
  inherited;
end;

procedure TTraceBinFile.InitializeFile;
var
  eofRec: TTraceRec;
  t: Integer;
begin
  Assert(FStorageSize > 0);
  Assert(Length(FFilename) > 0);
  Assign(FBinaryFile, Filename);
  FFileMode := fmOpenReadWrite;
  Rewrite(FBinaryFile);

  FBinaryFileOpen := True;

  FillChar(FHeaderRec, sizeof(TTraceRec), 0);
  FillChar(FFileRec, sizeof(TTraceRec), 0);
  FillChar(EofRec, sizeof(TTraceRec), 0);

  FLastIndex := 0;
  FHeaderRec.index := FLastIndex;
  FHeaderRec.tickcount := storageSize;
  SetMsg(@FHeaderRec, FTraceFileInfo);

  Write(FBinaryFile, FHeaderRec);
  for t := 1 to storageSize do
  begin
    Write(FBinaryFile, FFileRec);
  end;

  SetMsg(@eofRec, 'EOF');
  eofRec.index := traceEOFMARKER;
  Write(FBinaryFile, eofRec);
end;

procedure TTraceBinFile.Trace(msg: string);
// Write a trace message in circular file.
begin
  if (not FBinaryFileOpen) then
    exit;
  if (FFileMode = fmOpenRead) then
    exit; // not open for writing!
  Inc(FLastIndex);
  if (FLastIndex > FStorageSize) then
    FLastIndex := 1; // wrap around to 1 not zero! Very important!
  Seek(FBinaryFile, 0);
  FHeaderRec.index := FLastIndex;
  Write(FBinaryFile, FHeaderRec);
  FillChar(FFileRec, sizeof(TTraceRec), 0);
  Seek(FBinaryFile, FLastIndex);
  Inc(FAutoIncrementValue);
  if FAutoIncrementValue = 0 then
    FAutoIncrementValue := 1;
  FFileRec.index := FAutoIncrementValue;
  SetMsg(@FFileRec, msg);
  Write(FBinaryFile, FFileRec);
end;

procedure TTraceBinFile.OpenFile;
begin
  if FBinaryFileOpen then
  begin
    System.CloseFile(FBinaryFile);
    FBinaryFileOpen := False;
  end;
  if FileExists(FFilename) then
  begin
    //    System.FileMode :=fmOpenRead;
    FFileMode := fmOpenRead;
    AssignFile(FBinaryFile, FFilename);
    System.Reset(FBinaryFile); // open in current mode
    System.Seek(FBinaryFile, 0);
    Read(FBinaryFile, FHeaderRec);
    FLastIndex := FHeaderRec.index;
    FTraceFileInfo := string(FHeaderRec.Msg);
    FBinaryFileOpen := True;
  end
  else
  begin
    InitializeFile; // Creates the file.
  end;
end;

procedure TTraceBinFile.LoadTrace(traceStrs: TStrings);
var
  ReadAtIndex: Integer;
  Safety: Integer;

  procedure NextReadIndex;
  begin
    Inc(ReadAtIndex);
    if (ReadAtIndex > FStorageSize) then
      ReadAtIndex := 1; // wrap around to 1 not zero! Very important!
  end;

begin
  Assert(Assigned(traceStrs));
  traceStrs.Clear;

  if not FBinaryFileOpen then
  begin
    OpenFile;
  end;

  ReadAtIndex := FLastIndex;

  NextReadIndex;

  Safety := 0; // prevents endless looping.

  while True do
  begin
    if (ReadAtIndex = FLastIndex) or (Safety > FStorageSize) then
      break;
    Seek(FBinaryFile, ReadAtIndex);
    Read(FBinaryFIle, FFileRec);
    if FFileRec.msg[0] <> chr(0) then
    begin
      traceStrs.Add(FFileRec.msg);
    end;
    Inc(Safety);
    NextReadIndex;
  end;
end;

end.
于 2012-08-30T02:02:56.737 回答
2

看看这篇文章。

TraceTool 12.4:Trace 的瑞士军刀

于 2012-08-30T23:37:18.880 回答
1

我的建议是以日志文件每天“滚动”的方式实现您的日志记录。例如,在午夜,您的日志记录代码将您的日志文件(例如 MyLogFile.log)重命名为过时/归档版本(例如 MyLogFile-30082012.log),并开始一个新的空“实时”日志(例如再次 MyLogFile.log)。

那么这只是一个使用BareTail之类的东西来监视您的“实时”/每日日志文件的问题。

我接受这可能不是网络效率最高的方法,但它相当简单并且满足您的“实时”要求。

于 2012-08-29T22:41:25.867 回答