如何改进控制台应用程序入口点

时间:2013-03-05 15:09:21

标签: c# console-application main nlog

我一直在审查和重构一个用于开发执行一组作业的控制台应用程序的同事代码。我想知道如何改进系统的入口点,感觉它可能会更强大一些。我们使用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.");
    }
}

3 个答案:

答案 0 :(得分:3)

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

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

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

我已经将它用于执行时间操作并记录它们,方便一次性完成所有操作并节省重复代码。我的方法,ExecuteTimedAction(字符串logString,Action actionToExecute)使用秒表,记录起始字符串,启动秒表,执行方法(操作委托),停止秒表,并再次记录两个日志都有时间戳,程序集名称,以及呼叫从中发起的方法的名称。

获取方法的代码很简单,使用StackTrace对象,并获取上一次调用的StackFrame。

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

注意我上面有2个硬编码,但这是因为另外一个包装器调用;如果你直接打电话,那么你可能需要GetFrame(1)。最好的方法是使用立即窗口并尝试不同的帧,或者使用StackTrace对象的GetFrames()方法循环浏览它以查看你得到的内容。

我现在正在寻找保持字符串格式的参数并为日志包装器附加第一个Param。它可以这样做:

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

然后Log函数看起来像这样,因为你可以看到我的日志函数没有硬编码到ExecuteTimedAction中,所以我可以将任何日志操作传递给它。

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

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

希望这能让你在重构时获得足够的食物!

答案 1 :(得分:2)

我并不特别喜欢这种包装NLog的方式。 GetMethodName没有理由。 NLog能够自动提供方法名称和类名(通过正确配置布局)。在包装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的所有实例将共享同一个Logger实例)。由于记录器是以其类命名的,因此您可以更好地控制日志输出的生成方式。您可以配置NLog(通过NLog.config),以便所有记录器始终记录。或者您可以对其进行配置,使得只有某些记录器记录(或者某些记录器记录在一个级别而其他记录器记录在不同级别)。假设您有一个包含各种组件的程序。它们似乎都工作正常,但你必须实现一个新的组件。在开发过程中,您可能希望将其日志记录方式调高(即获取更多信息),同时将程序的其他部分关闭(即从程序中正常工作的部分获取miminal信息)。此外,您可以通过记录器名称重定向日志记录(例如,将所有日志消息从某个类或命名空间发送到某个日志记录目标(如果您正在调试程序的那部分,可能是调试器目标)并发送其他日志消息(包括那些去调试器)到您的输出文件或数据库)。如果您有一个静态记录器包装器,则您无法在每个类或每个命名空间的基础上控制日志记录。

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

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

答案 2 :(得分: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...,