0

一連のジョブを実行するコンソール アプリケーションを開発するために、同僚のコードを見直してリファクタリングしてきました。システムへのエントリ ポイントを改善する方法について意見を求めます。コンソールとログ ファイルに自動的に表示されるように構成されているログには、NLog を使用します。同様に、catch (Exception ex)すり抜けた例外をきれいに処理してログに記録する必要があります。理論的には、例外が発生することはありませんが、可能な場合はこれらをきれいに処理することが常に最善です。

{0}:各呼び出しの開始時にログを記録するスタイルには特に不満があり_logger.Info()ますが、そのように独自の関数にリファクタリングされた場合LogMe(methodName, "text to be logged")、それほど多くの入力を節約することはできません。スレッドを維持するコードなどを意図的に除外していることに注意してください。これは、私が探している範囲を超えています。

以下を改善できますか、またはこれは、多大な労力/リファクタリングなしで合理的に可能な限り「良い」ものですか?

static void Main(string[] args)
{
    string methodName = string.Format("{0}.Main()", typeof(Program).FullName);
    try
    {
        _logger.Info("{0}: Launched", methodName);
        IKernel kernel = IOC.SetupKernel();

        _logger.Info("{0}: Reading job schedules from the configuration file");
        JobScheduleSection scheduleSection = (JobScheduleSection)ConfigurationManager.GetSection("jobScheduleSection");
        if (scheduleSection == null)
        {
            _logger.Warn("{0}: No job schedule section found in configuration file", methodName);
            return;
        }

        List<IJobSchedule> schedules = scheduleSection.JobSchedules.ToList();
        if (schedules == null)
        {
            _logger.Info("{0}: No job schedules found", methodName);
            return;
        }
        _logger.Info("{0}: Found {1} job schedules", methodName, schedules.Count);

        _logger.Info("{0}: Kicking Launcher...", methodName);
        Launcher launcher = new Launcher(kernel, schedules);
        launcher.LaunchSchedulerService();
    }
    catch (Exception ex)
    {
        _logger.ErrorException(string.Format("{0}: An unhandled exception occurred", methodName), ex);
    }
    finally
    {
        _logger.Info("{0}: Exited. Program complete.");
    }
}
4

3 に答える 3

3

私がこれを行った方法は、各ログメソッドをラップするNLogのラッパークラスを作成し、methodNameを廃止し、StackTraceオブジェクトを使用してメソッド名を取得することです。そうすれば、毎回書く必要はありません。Loggingラッパーメソッドを呼び出すメソッドのメソッド名が自動的に挿入されます。

{0}とmethodNameがどこにもないので、見た目がすっきりします。

さらに一歩進んで、ログ文字列とアクションを取得し、アクションを実行し、StackTraceオブジェクトを使用してログオブジェクトを一度に呼び出すロギングラッパークラスを作成できます。

私はこれをタイムアクションの実行とログ記録に使用しました。すべてを1回の呼び出しで実行すると便利で、繰り返しのコードを節約できます。私のメソッドExecuteTimedAction(string logString、Action actionToExecute)は、ストップウォッチを使用し、開始文字列をログに記録し、ストップウォッチを開始し、メソッド(アクションデリゲート)を実行し、ストップウォッチを停止し、両方のログにタイムスタンプとアセンブリの名前を付けて再度ログに記録します、および呼び出しが開始されたメソッドの名前。

メソッドを取得するためのコードは単純で、StackTraceオブジェクトを使用して、前の呼び出しのStackFrameを取得します。

        var stackTrace = new StackTrace();
        var callingMethodName = stackTrace.GetFrame(2).GetMethod().Name;

上記に2つハードコードされていることに注意してください。これは、追加のラッパー呼び出しが原因です。直接呼び出す場合は、代わりにGetFrame(1)が必要になる場合があります。最良の方法は、即時ウィンドウを使用してさまざまなフレームを試すか、StackTraceオブジェクトのGetFrames()メソッドを使用して、フレームをループして何が得られるかを確認することです。

文字列形式のパラメータを保持し、ログラッパーの最初のパラメータを追加することを検討しています。これは次のように実行できます。

public static class LogWrapper
{
    private static Logger _logger // where Logger assumes that is the actual NLog logger, not sure if it is the right name but this is for example

    public static void Info(string logString, object[] params)
    {
        // Just prepend the method name and then pass the string and the params to the NLog object
        _logger.Info(
            string.Concat(
                GetMethodName(),
                ": ",
                logString
            ),
            params
        );
    }

    public static void Warn(string logString, object[] params)
    {
        // _logger.Warn(
        //  You get the point ;)
        // )
    }

    private static string GetMethodName()
    {
        var stackTrace = new StackTrace(); // Make sure to add using System.Diagnostics at the top of the file
        var callingMethodName = stackTrace.GetFrame(2).GetMethod().Name; // Possibly a different frame may have the correct method, might not be 2, might be 1, etc.
    }
}

次に、呼び出しコードで、_loggerメンバーはLoggerではなくLoggerWrapperになり、まったく同じ方法で呼び出しますが、コードから{0}を削除します。nullをチェックする必要があります。他にパラメータがない場合は、パラメータなしで呼び出すだけのメソッドオーバーロードが必要です。NLogがそれをサポートしているかどうかはわかりませんので、これを確認する必要があります。

... 編集:

興味深い点として、このタイプのコードは、一連のアセンブリによって参照される可能性のある一般的なライブラリタイプのアセンブリで使用しているため、ハードコーディングしたり心配したりすることなく、アセンブリの呼び出しやメソッド名などの情報を取得できます。私のロギングコードでそれ。また、コードを使用している他の人がそれについて心配する必要がないことも確認します。Log()やWarn()などを呼び出すだけで、アセンブリが自動的にログに保存されます。

ここに例があります(あなたがあなたのためにやり過ぎだと言ったのは知っていますが、あなたがこのようなものを必要とするかもしれないなら、将来のための思考のための食べ物です)。この例では、メソッド名ではなく、アセンブリのみをログに記録していましたが、簡単に組み合わせることができます。

    #region :           Execute Timed Action                        :

    public static T ExecuteTimedAction<T>(string actionText, Func<T> executeFunc)
    {
        return ExecuteTimedAction<T>(actionText, executeFunc, null);
    }

    /// <summary>
    /// Generic method for performing an operation and tracking the time it takes to complete (returns a value)
    /// </summary>
    /// <typeparam name="T">Generic parameter which can be any Type</typeparam>
    /// <param name="actionText">Title for the log entry</param>
    /// <param name="func">The action (delegate method) to execute</param>
    /// <returns>The generic Type returned from the operation's execution</returns>

    public static T ExecuteTimedAction<T>(string actionText, Func<T> executeFunc, Action<string> logAction)
    {
        string beginText = string.Format("Begin Execute Timed Action: {0}", actionText);

        if (null != logAction)
        {
            logAction(beginText);
        }
        else
        {
            LogUtil.Log(beginText);
        }

        Stopwatch stopWatch = Stopwatch.StartNew();
        T t = executeFunc(); // Execute the action
        stopWatch.Stop();

        string endText = string.Format("End Execute Timed Action: {0}", actionText);
        string durationText = string.Format("Total Execution Time (for {0}): {1}", actionText, stopWatch.Elapsed);

        if (null != logAction)
        {
            logAction(endText);
            logAction(durationText);                
        }
        else
        {
            LogUtil.Log(endText);
            LogUtil.Log(durationText);
        }

        return t;
    }

    public static void ExecuteTimedAction(string actionText, Action executeAction)
    {
        bool executed = ExecuteTimedAction<bool>(actionText, () => { executeAction(); return true; }, null);
    }

    /// <summary>
    /// Method for performing an operation and tracking the time it takes to complete (does not return a value)
    /// </summary>
    /// <param name="actionText">Title for the log entry</param>
    /// <param name="action">The action (delegate void) to execute</param>

    public static void ExecuteTimedAction(string actionText, Action executeAction, Action<string> logAction)
    {
        bool executed = ExecuteTimedAction<bool>(actionText, () => { executeAction(); return true; }, logAction);
    }

    #endregion

次に、ログ関数は次のようになります。ログ関数がExecuteTimedActionにハードコードされていないことがわかるので、ログアクションを渡すことができます。

ログクラスでは、エントリアセンブリ名を静的変数に一度保存し、それをすべてのログに使用します...

private static readonly string _entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name;

これにより、リファクタリングについて考えるのに十分な食べ物が得られることを願っています!

于 2013-03-05T15:26:23.307 に答える
2

私は NLog をラップする方法が特に好きではありません。GetMethodName の理由はありません。NLog には、(レイアウトを正しく構成することにより) メソッド名とクラス名を自動的に提供する機能があります。NLog (または log4net については log4net) をラップするときの鍵は、NLog.Logger.Log に関してロギング メソッド (Info、Trace、Debug) を実装することです。Log へのパラメーターの 1 つは、ロガーのタイプ (つまり、NLog ラッパーのタイプ) です。NLog がメソッド名を書き出す場合、その型が見つかるまでスタック トレースをたどります。それが「ロガー」とアプリケーションの境界になります。スタック トレースをもう 1 ステップ上げると、呼び出しサイトのスタックを取得できます。これにより、NLog はメソッド名とクラス名をログに記録できます。

また、静的 NLog ラッパーの問題は、ロガー名を持つ機能が失われることです。通常、ロガーを取得するためのパターンは、ログを記録するすべてのクラスに次のようなコードを含めることです。

public class MyClassFromWhichIWantToLog
{
  private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

  public void DoSomething()
  {
    _logger.Info("Hello!");
  }
}

LogManager.GetCurrentClassLogger は、「名前」がクラスの完全修飾クラス名である Logger のインスタンスを返します。ロガーを保持するために静的クラス変数を使用しているため、型ごとに 1 つのロガー インスタンスがあります (つまり、MyClassFromWhichIWantToLog のすべてのインスタンスは、Logger の同じインスタンスを共有します)。ロガーにはそのクラスの名前が付けられているため、ログ出力の生成方法をより詳細に制御できます。すべてのロガーが常にログを記録するように (NLog.config を介して) NLog を構成できます。または、特定のロガーのみがログを記録するように構成できます (または、一部のロガーはあるレベルでログを記録し、他のロガーは別のレベルでログを記録します)。さまざまなコンポーネントを持つプログラムがあるとします。それらはすべて正常に機能しているように見えますが、新しいコンポーネントを実装する必要があります。開発中は、ロギングの方法を上げて (つまり、より多くの情報を取得する)、プログラムの他の部分を下げて (つまり、正常に動作しているプログラムの部分から最低限の情報を取得) したい場合があります。また、ロガー名でロギングをリダイレクトできます (たとえば、特定のクラスまたは名前空間から特定のロギング ターゲット (プログラムのその部分をデバッグしている場合はデバッガー ターゲット) にすべてのロギング メッセージを送信) や、他のログ (デバッガー) を出力ファイルまたはデータベースに)。静的ロガー ラッパーを使用している場合は、クラスごとまたは名前空間ごとにログを制御できなくなります。ロガー名でロギングをリダイレクトできます (たとえば、特定のクラスまたは名前空間から特定のロギング ターゲット (プログラムのその部分をデバッグしている場合はデバッガー ターゲット) にすべてのロギング メッセージを送信) や、他のログ (デバッガーに送信されるものを含む) を送信できます。 ) 出力ファイルまたはデータベースに)。静的ロガー ラッパーを使用している場合は、クラスごとまたは名前空間ごとにログを制御できなくなります。ロガー名でロギングをリダイレクトできます (たとえば、特定のクラスまたは名前空間から特定のロギング ターゲット (プログラムのその部分をデバッグしている場合はデバッガー ターゲット) にすべてのロギング メッセージを送信) や、他のログ (デバッガーに送信されるものを含む) を送信できます。 ) 出力ファイルまたはデータベースに)。静的ロガー ラッパーを使用している場合は、クラスごとまたは名前空間ごとにログを制御できなくなります。

この質問に対する私の答えを見てください。

NLog をラップするときにコールサイト情報を保持する方法

私の答えは、正しい呼び出しサイト情報を維持する NLog ラッパーのソース コード (NLog のソース リポジトリから直接) を提供します。NLog の例は、NLog.Logger をラップするのではなく、("EventID" を追加して) 拡張する方法を説明するためのものであることに注意してください。EventID を無視すると、ラッパーのタイプを NLog の Logger.Log メソッドに渡すことが重要であることがわかります。

これは非常に簡素化された NLog ラッパー (単一のメソッド (Info) のみ) であり、呼び出しサイト情報が保持されるように NLog を正しくラップする必要があります。

  public class MyLogger
  {
    public MyLogger(Logger logger)
    {
      _logger = logger;
    }

    private Logger _logger;
    private void WriteMessage(LogLevel level, string message)
    {
      //
      // Build LogEvent here...
      //
      LogEventInfo logEvent = new LogEventInfo(logLevel, context.Name, message);
      logEvent.Exception = exception;

      //
      // Pass the type of your wrapper class here...
      //
      _logger.Log(typeof(MyLogger), logEvent);
    }

    public void Info(string message)
    {
      WriteMessage(LogLevel.Info, message);
    }
  }

次のように使用します。

public class MyClassWhereIWantToUseLogging
{
  private static readonly _logger = new MyLogger(LogManager.GetCurrentClassLogger());

  public void DoSomething()
  {
    _logger.Info("Hello!"); //If you log call site info, you should class name and method name.
  }
}

NLog の詳細については、この人気のある (私がそう言うなら ;-)) NLog の投稿を参照してください。

最も有用な NLog 構成

于 2013-03-08T22:00:57.453 に答える
1

アップデート

NLogクラスを拡張しようとしたり、メソッド/メソッドのオーバーロードを作成したりする代わりに、これに対するはるかにクリーンな解決策を見つけました。NLogは、NLog.configアプリケーションとともにデプロイされたファイルに追加された次のフィールドをサポートしていました。

layout="${callsite}"

これは、CSV、コンソール、電子メールなど、適切なターゲットに適用できます。CSVでは、次のように構成されます。

<target name="CSVFile" xsi:type="File"  fileName="${basedir}/Logging/BullorBear.Identity.API-${date:format=yyyy-MM-dd}.csv" 
        archiveEvery="Day" maxArchiveFiles="28">
  <layout xsi:type="CSVLayout">
    <column name="Index" layout="${counter}" />
    <column name="Time" layout="${longdate}" />
    <column name="Callsite" layout="${callsite}" />
    <column name="Severity" layout="${level:uppercase=true}" />
    <column name="Detail" layout="${message}" />
    <column name="Exception" layout="${exception:format=ToString}" />
  </layout>
</target>

出力;

Index,Time,Callsite,Severity,Detail,Exception
1,2013-03-12 12:35:07.6890,ProjectName.Controllers.SomeController.SomeMethod,INFO,Authenticating...,
于 2013-03-12T12:38:58.853 に答える