【问题标题】:How to improve console application entry point如何改进控制台应用程序入口点
【发布时间】:2013-02-20 01:20:04
【问题描述】:

我一直在审查和重构一位同事的代码,用于开发执行一组作业的控制台应用程序。我想就如何改进系统的入口点提出意见,感觉它可能会更健壮一些。我们使用 NLog 进行日志记录,它被配置为自动显示在控制台和日志文件上。同样,我有一个catch (Exception ex) 来尝试干净地处理并记录任何漏掉的异常——理论上它不应该被击中,但最好在可能的情况下干净地处理这些事情。

我对在每个 _logger.Info() 调用开始时使用 {0}: 的日志记录风格特别不满意,但如果像 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.");
    }
}

【问题讨论】:

  • 想知道这是否最适合代码审查堆栈交换:codereview.stackexchange.com ....
  • @Arran,可能,但我不打算准确审查它 - 但如果被问到我会移动它

标签: c# console-application main nlog


【解决方案1】:

我这样做的方法是为 NLog 创建一个包装类,它将包装每个日志方法,并混淆 methodName 并使用 StackTrace 对象获取方法名称。那你就不用每次都写了;调用 Logging 封装方法的方法的方法名是自动注入的。

它看起来更干净,因为您不会到处都有 {0} 和 methodName。

您甚至可以更进一步,创建一个日志包装类,该类接受日志字符串和一个动作,执行该动作,并使用 StackTrace 对象一次性调用日志对象。

我已经使用它来执行时间操作并记录它们,一次调用即可完成所有操作并节省重复代码。我的方法 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 成员变为 LoggerWrapper,而不是 Logger,并且您以完全相同的方式调用它,但您从代码中删除了 {0}。您需要检查空值,也许如果没有其他参数,则有一个方法重载,只在没有参数的情况下调用;我不确定 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 中,所以我可以将任何日志操作传递给它。

在日志类中,我将 Entry 程序集名称保存在静态变量中一次,并将其用于所有日志...

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

希望这能让您在重构方面有足够的思考空间!

【讨论】:

  • 包装记录器的有趣方法 - 你有一些简短的示例代码吗?对于我们不需要的东西来说,秒表方法有点矫枉过正,但对于更原子的、基于任务的部分,我肯定会记住这一点,因为记录器已经配置为记录每个条目的日期/时间
  • 有趣!这对性能有何影响或有吗?项目编译发布是否会抛出异常?
  • 刚刚添加了一些示例。不知道性能会受到什么影响,但是如果日志记录和代码可读性/维护比性能更重要,那么如果值得的话,您必须进行调用。主要开销是使用 stackFrame 对象,但我从未对其进行基准测试。它确实使代码更具可读性,而且由于我不是在编写股票交易应用程序,所以我认为小幅打击是可以接受的。这始终是编程中的权衡。
  • @DiskJunky - 好的,我想我已经添加了足够的内容来获得一些想法:) 祝你好运。
【解决方案2】:

我不是特别喜欢这种包装 NLog 的方式。 GetMethodName 没有理由。 NLog 能够自动提供方法名和类名(通过正确配置 Layout)。在包装 NLog(或 log4net)时,关键是根据 NLog.Logger.Log 实现日志记录方法(Info、Trace、Debug)。 Log 的参数之一是记录器的类型(即 NLog 包装器的类型)。当 NLog 想要写出方法名时,它会简单地向上遍历堆栈跟踪,直到找到该类型。这将是“记录器”和应用程序之间的边界。堆栈跟踪再上一步,您可以从中获取调用站点的堆栈。 NLog 可以记录方法名和类名。

此外,静态 NLog 包装器的问题是您无法拥有记录器名称。通常,检索记录器的模式是在您可能想要记录的每个类中都有这样的代码:

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

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

LogManager.GetCurrentClassLogger 返回一个 Logger 实例,其“name”是类的完全限定类名。因为我们使用静态类变量来保存记录器,所以每种类型都有一个记录器实例(即 MyClassFromWhichIWantToLog 的所有实例将共享同一个记录器实例)。因为记录器以其类命名,所以您可以更好地控制日志输出的生成方式。您可以配置 NLog(通过 NLog.config),以便所有记录器始终记录。或者您可以将其配置为仅记录某些记录器(或某些记录器在一个级别记录,而其他记录器在不同级别记录)。假设您有一个包含各种组件的程序。它们似乎都工作正常,但您必须实现一个新组件。在开发过程中,您可能希望将其日志记录方式调高(即获取更多信息),同时将程序的其他部分调低(即从运行正常的程序部分获取最少的信息)。此外,您可以通过记录器名称重定向您的日志记录(例如,将来自某个类或命名空间的所有日志记录消息发送到某个日志记录目标(如果您正在调试程序的该部分,则可能是调试器目标)并发送其他(包括那些将调试器)到您的输出文件或数据库)。如果您有一个静态记录器包装器,那么您将无法控制基于每个类或每个命名空间的日志记录。

看看我对这个问题的回答:

How to retain callsite information when wrapping 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 帖子:

Most useful NLog configurations

【讨论】:

    【解决方案3】:

    更新

    我找到了一个更简洁的解决方案,而不是尝试扩展 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...,
    

    【讨论】:

      猜你喜欢
      • 2019-03-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-18
      • 1970-01-01
      • 2016-11-16
      • 1970-01-01
      相关资源
      最近更新 更多