我一直在审查和重构一个同事的代码,以开发一个执行一组作业的控制台应用程序。我想听听关于如何改进系统入口点的意见,感觉它可以更健壮一点。我们使用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.");
}
}
我这样做的方式是为NLog创建一个包装器类,它将包装每个日志方法,并模糊methodName,并使用StackTrace对象来获取方法名称。这样你就不用每次都写了;调用日志包装器方法的方法的方法名将自动注入。
这样就不会到处都是{0}和methodName了。
你甚至可以更进一步,创建一个日志包装类,它接受日志字符串和一个Action,执行Action,并使用StackTrace对象调用日志对象。
我已经使用这个来执行时间动作并记录它们,在一个调用中完成所有操作很方便,并且节省了重复的代码。我的方法executetimmedaction (string logString, Action actionToExecute)使用一个秒表,记录一个开始字符串,启动秒表,执行方法(Action delegate),停止秒表,并再次记录日志,两个日志都有时间戳、程序集名称和调用发起的方法名称。
获取方法的代码很简单,使用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
然后Log函数看起来像这样,你可以看到我的日志函数没有硬编码到executetimmedaction,所以我可以传递任何日志操作给它。
在日志类中,我将Entry程序集名称保存在一个静态变量中,并将其用于所有日志…
private static readonly string _entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name;
希望这能给你足够的思考重构的食物!
我不是特别喜欢这种包装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"是类的完全限定类名。因为我们使用静态类变量来保存记录器,所以每种类型都有一个记录器实例(即myclassfromwherhiwanttolog的所有实例将共享同一个logger实例)。由于日志记录器以其类命名,因此您可以更好地控制如何生成日志记录输出。你可以配置NLog(通过NLog.config),这样所有的记录器都会记录所有时间。或者您可以配置它,以便只有某些记录器记录日志(或者一些记录器在一个级别记录日志,而其他记录器在不同级别记录日志)。假设你有一个程序,它有不同的组成部分。它们看起来都工作得很好,但是您必须实现一个新的组件。在开发过程中,你可能想要将其日志记录方式调高(即获取更多信息),同时将程序的其他部分调低(即从程序正常工作的部分获取最少信息)。此外,您可以通过记录器名称重定向日志记录(例如,将所有日志记录消息从某个类或名称空间发送到某个日志记录目标(如果您正在调试程序的那一部分,可能是调试器目标),并将其他消息(包括那些要到调试器的消息)发送到您的输出文件或数据库)。如果您有一个静态日志记录器包装器,您就失去了在每个类或每个名称空间的基础上控制日志记录的能力。
看看我对这个问题的回答:
如何在包装NLog时保留callsite信息
我的回答提供源代码(直接从NLog的源存储库)的NLog包装维护正确的调用站点信息。请注意,NLog中的示例更多地说明了如何扩展NLog。记录器(通过添加"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配置
更新
我找到了一个更干净的解决方案,而不是试图扩展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...,