// *************************************************************************** }
//
// LoggerPro
//
// Copyright (c) 2010-2024 Daniele Teti
//
// https://github.com/danieleteti/loggerpro
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ***************************************************************************

unit LoggerPro.RESTAppender;

interface

uses
  Classes,
  SysUtils,
  LoggerPro,
  System.Net.HttpClient;

type

  {
    Log appender for a REST endpoint
    Author: Daniele Teti (https://github.com/danieleteti/)
    Some ideas from NSQ appender from Stéphane "Fulgan" GROBETY (https://github.com/Fulgan/)
  }

  TOnCreateData = reference to procedure(const Sender: TObject; const LogItem: TLogItem; const ExtendedInfo: TLoggerProExtendedInfo;
    var Data: TStream);
  TOnNetSendError = reference to procedure(const Sender: TObject; const LogItem: TLogItem; const NetError: Exception;
    var RetryCount: Integer);

  TLoggerProRESTAppender = class(TLoggerProAppenderBase, ILogAppender)
  strict private
    FOnCreateData: TOnCreateData;
    FOnNetSendError: TOnNetSendError;
    fExtendedInfo: TLoggerProExtendedInfo;
    fContentType: string;
    fRESTUrl: string;
    {.$IFDEF MSWINDOWS}
    fExtendedInfoData: array [low(TLogExtendedInfo) .. high(TLogExtendedInfo)] of string;
    {.$ENDIF}
    procedure SetOnCreateData(const Value: TOnCreateData);
    procedure SetOnNetSendError(const Value: TOnNetSendError);
  strict protected
    procedure LoadExtendedInfo;
    function GetExtendedInfo: string;
  protected const
    { @abstract(Defines the default format string used by the @link(TLoggerProRESTAppender).)
      The positional parameters are the following:
      @orderedList(
      @item SetNumber 0
      @item TimeStamp
      @item ThreadID
      @item LogType
      @item LogMessage
      @item Extended Information
      @item LogTag
      )
    }
    DEFAULT_LOG_FORMAT = '%0:s [TID %1:10u][%2:-8s] %3:s {EI%4:s}[%5:s]';
    DEFAULT_EXTENDED_INFO = [TLogExtendedInfo.EIUserName, TLogExtendedInfo.EIComputerName, TLogExtendedInfo.EIProcessName,
      TLogExtendedInfo.EIProcessID, TLogExtendedInfo.EIDeviceID];
    DEFAULT_REST_URL = 'http://127.0.0.1:8080/api/logs';
    procedure InternalWriteLog(const aURI: string; const aLogItem: TLogItem; const aStream: TStream);
  public
    function GetRESTUrl: string;
    procedure SetRESTUrl(const Value: string);
    procedure WriteLog(const aLogItem: TLogItem); override;
    constructor Create(aRESTUrl: string = DEFAULT_REST_URL; aContentType: string = 'text/plain';
      aLogExtendedInfo: TLoggerProExtendedInfo = DEFAULT_EXTENDED_INFO; aLogItemRenderer: ILogItemRenderer = nil); reintroduce;
    property RESTUrl: string read GetRESTUrl write SetRESTUrl;
    property OnCreateData: TOnCreateData read FOnCreateData write SetOnCreateData;
    property OnNetSendError: TOnNetSendError read FOnNetSendError write SetOnNetSendError;
    procedure TearDown; override;
    procedure Setup; override;
    function CreateData(const SrcLogItem: TLogItem): TStream; virtual;
  end;

implementation

uses
  System.NetEncoding,
  System.IOUtils,
  System.Net.URLClient
{$IF Defined(MSWINDOWS) }
    ,
  Winapi.Windows
{$ENDIF}
{$IF Defined(Android) }
    ,
  Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.JavaTypes,
  Androidapi.JNI.Os,
  Androidapi.JNI.Util,
  Androidapi.Helpers
{$ENDIF}
    ;

{$IFDEF MSWINDOWS }


function GetUserFromWindows: string;
var
  iLen: Cardinal;
begin
  iLen := 256;
  Result := StringOfChar(#0, iLen);
  GetUserName(PChar(Result), iLen);
  SetLength(Result, iLen - 1);
end;

function GetComputerNameFromWindows: string;
var
  iLen: Cardinal;
begin
  iLen := MAX_COMPUTERNAME_LENGTH + 1;
  Result := StringOfChar(#0, iLen);
  GetComputerName(PChar(Result), iLen);
  SetLength(Result, iLen);
end;

{$ENDIF}


constructor TLoggerProRESTAppender.Create(aRESTUrl: string; aContentType: string;
  aLogExtendedInfo: TLoggerProExtendedInfo; aLogItemRenderer: ILogItemRenderer);
begin
  inherited Create(aLogItemRenderer);
  fRESTUrl := aRESTUrl;
  fExtendedInfo := aLogExtendedInfo;
  fContentType := aContentType;
  LoadExtendedInfo;
end;

function TLoggerProRESTAppender.CreateData(const SrcLogItem: TLogItem): TStream;
begin
  Result := nil;
  try
    if Assigned(FOnCreateData) then
    begin
      FOnCreateData(Self, SrcLogItem, fExtendedInfo, Result);
    end
    else
    begin
      Result := TStringStream.Create(FormatLog(SrcLogItem), TEncoding.UTF8);
    end;
  except
    on E: Exception do
    begin
      FreeAndNil(Result);
      raise;
    end;
  end;
end;


{TODO -oDanieleT -cGeneral : Currently ExtendedInfo are not logged}
function TLoggerProRESTAppender.GetExtendedInfo: string;
begin
  Result := '';

{$IFDEF MSWINDOWS}
  if TLogExtendedInfo.EIUserName in fExtendedInfo then
  begin
    Result := Result + ';UserName=' + fExtendedInfoData[TLogExtendedInfo.EIUserName];
  end;
  if TLogExtendedInfo.EIComputerName in fExtendedInfo then
  begin
    Result := Result + ';ComputerName=' + fExtendedInfoData[TLogExtendedInfo.EIComputerName];
  end;
  if TLogExtendedInfo.EIProcessName in fExtendedInfo then
  begin
    Result := Result + ';ProcessName=' + fExtendedInfoData[TLogExtendedInfo.EIProcessName];
  end;
  if TLogExtendedInfo.EIProcessID in fExtendedInfo then
  begin
    Result := Result + ';PID=' + fExtendedInfoData[TLogExtendedInfo.EIProcessID];
  end;
{$ENDIF}
{$IF Defined(Android)}
  if TLogExtendedInfo.EIProcessName in fExtendedInfo then
  begin
    Result := Result + ';ProcessName=' + fExtendedInfoData[TLogExtendedInfo.EIProcessName];
  end;
{$ENDIF}
  Result := '[' + Result.Substring(1) + ']';
end;

function TLoggerProRESTAppender.GetRESTUrl: string;
begin
  Result := fRESTUrl;
end;

procedure TLoggerProRESTAppender.LoadExtendedInfo;
begin
{$IF DEFINED(MSWINDOWS)}
  if TLogExtendedInfo.EIProcessID in fExtendedInfo then
  begin
    fExtendedInfoData[TLogExtendedInfo.EIProcessID] := IntToStr(GetCurrentProcessId);
  end;
  if TLogExtendedInfo.EIUserName in fExtendedInfo then
  begin
    fExtendedInfoData[TLogExtendedInfo.EIUserName] := GetUserFromWindows;
  end;
  if TLogExtendedInfo.EIComputerName in fExtendedInfo then
  begin
    fExtendedInfoData[TLogExtendedInfo.EIComputerName] := GetComputerNameFromWindows;
  end;
  if TLogExtendedInfo.EIProcessName in fExtendedInfo then
  begin
    fExtendedInfoData[TLogExtendedInfo.EIProcessName] := TPath.GetFileName(GetModuleName(HInstance));
  end;
  if TLogExtendedInfo.EIProcessID in fExtendedInfo then
  begin
    fExtendedInfoData[TLogExtendedInfo.EIProcessID] := IntToStr(GetCurrentProcessId);
  end;
{$ENDIF}
{$IF Defined(Android)}
  if TLogExtendedInfo.EIProcessName in fExtendedInfo then
  begin
    fExtendedInfoData[TLogExtendedInfo.EIProcessName] := TAndroidHelper.ApplicationTitle;
  end;
{$ENDIF}
end;

procedure TLoggerProRESTAppender.SetRESTUrl(const Value: string);
begin
  fRESTUrl := Value;
end;

procedure TLoggerProRESTAppender.SetOnCreateData(const Value: TOnCreateData);
begin
  FOnCreateData := Value;
end;

procedure TLoggerProRESTAppender.SetOnNetSendError(const Value: TOnNetSendError);
begin
  FOnNetSendError := Value;
end;

procedure TLoggerProRESTAppender.Setup;
begin
  inherited;
end;

procedure TLoggerProRESTAppender.TearDown;
begin
  inherited;
end;

procedure TLoggerProRESTAppender.InternalWriteLog(const aURI: string; const aLogItem: TLogItem; const aStream: TStream);
var
  lHTTPCli: THTTPClient;
  lRetryCount: Integer;
  lResp: IHTTPResponse;
const
  MAX_RETRY_COUNT = 5;
begin
  lRetryCount := 0;
  lHTTPCli := THTTPClient.Create;
  try
    if Assigned(aStream) then
    begin
      repeat
        try
{$IF CompilerVersion >= 31}
          lHTTPCli.ConnectionTimeout := 1000;
          lHTTPCli.ResponseTimeout := 3000;
{$ENDIF}
          aStream.Seek(0, soFromBeginning);
          lResp := lHTTPCli.Post(aURI, aStream, nil, [TNetHeader.Create('content-type', fContentType)]);
          if not(lResp.StatusCode in [200, 201]) then
          begin
            raise ELoggerPro.Create(lResp.ContentAsString);
          end;
          Break;
        except
          on E: Exception do
          begin
            // if there is an event handler for net exception, call it
            if Assigned(FOnNetSendError) then
              OnNetSendError(Self, aLogItem, E, lRetryCount);
            Inc(lRetryCount);
            // if the handler has set FRetryCount to a positive value then retry the call
            if lRetryCount >= MAX_RETRY_COUNT then
              break;
          end;
        end;
      until False;
    end;
  finally
    FreeAndNil(lHTTPCli);
  end;
end;

procedure TLoggerProRESTAppender.WriteLog(const aLogItem: TLogItem);
var
  lURI: string;
  lData: TStream;
begin
  lURI := RESTUrl + '/' + TNetEncoding.URL.Encode(aLogItem.LogTag.Trim) + '/' + TNetEncoding.URL.Encode(aLogItem.LogTypeAsString);
  lData := CreateData(aLogItem);
  try
    if Assigned(lData) then
      InternalWriteLog(lURI, aLogItem, lData);
  finally
    lData.Free;
  end;
end;

end.
