使用ActivityId (System.Diagnostics)对应用程序跟踪进行分组






4.33/5 (3投票s)
我不需要强调跟踪(日志记录)在任何应用程序中的重要性。没有日志,我们常常无法诊断故障原因。日志还有助于我们跟踪应用程序随时间的行为和使用情况。
引言
我不需要强调跟踪(日志记录)在任何应用程序中的重要性。没有日志,我们常常无法诊断故障原因。日志还有助于我们跟踪应用程序随时间的行为和使用情况。幸运的是,有很多优秀的库(包括.NET框架本身)可以帮助我们创建有意义的日志。在这篇文章中,我想向您展示如何使用System.Diagnostics
类,通过它们对应的活动很好地对应用程序跟踪进行分组。
让我们假设我们需要编写一个订单系统。它的基本任务是获取描述订单的一些数据,进行一些处理(处理付款,从数据库或其他系统收集附加数据,发送邮件等),并将订单放入数据库。根据“进行一些处理”阶段的复杂程度,有些地方可能会出错。如果发生这种情况,您可以告知客户他的订单无法处理并要求他重试(但这真的是他的错吗?你的系统有bug?)或者您可以将订单保存在数据库中,并附带一些错误状态和错误消息,然后向您的支持人员寻求进一步调查。如果您关心您的客户,您可能会采用第二种方法。因此,支持人员最终会收到一条错误消息,他需要从中推断出故障的真正原因。错误消息很可能是异常名称及其堆栈跟踪,对于生产系统(由于RELEASE构建)而言,这可能并非真正有用。所以我们需要某种跟踪来了解系统中断前发生了什么。请看下面代码中的PlaceOrder
方法
public sealed class OrderingSystem
{
private const int MaxStatusMessageLength = 2000;
private static readonly TraceSource logger = new TraceSource("OrderingSystem");
public int PlaceOrder(String username, String product, decimal cash)
{
// create an order record
var order = new Order {
Username = username,
Product = product,
Cash = cash,
CreatedOn = DateTime.UtcNow,
};
// validation: if order is invalid throw some exception - ask client to correct
if (String.IsNullOrEmpty(username)) {
logger.TraceEvent(TraceEventType.Warning, 0,
"Invalid username in order: {0}", username);
}
logger.TraceEvent(TraceEventType.Information, 0, "Order validated successfully");
try {
// connect with the storehouse system and reserve one item
var storehouseUrl =
"http://storehouse.example.com/ReserveItem?product=" + product;
logger.TraceInformation("Connecting with the ordering system: {0}",
storehouseUrl);
// TODO: actual connection with the url
// send email to the delivery (or create a task or database record etc.)
var email = "chiefOfDelivery@example.com";
logger.TraceInformation("Informing a chief of delivery: {0}", email);
// TODO: send email
order.Status = "OK";
} catch (Exception ex) {
logger.TraceEvent(TraceEventType.Critical, 0,
"Exception occured while trying to place the order: {0}", ex);
order.Status = "FAIL";
order.StatusMessage = ex.ToString();
if (order.StatusMessage.Length > MaxStatusMessageLength) {
order.StatusMessage = order.StatusMessage.Substring
(0, MaxStatusMessageLength);
}
}
// save record to the database
using (var conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["conn"].ConnectionString)) {
conn.Open();
order.Id = (int)conn.Insert(order);
return order.Id;
}
}
}
您是否看到了此代码生成的日志存在的问题?毕竟,订单在数据库中,我们有非常详细的日志,因此我们应该能够调查问题。现在,想象一下,此方法每秒钟被调用20次,因此日志记录开始快速增长。您将如何查找表征特定订单的行?这将非常困难,不是吗?为了解决这个问题,我们可以将订单ID附加到我们生成的每条日志消息中(因此我们需要在许多地方更改代码)——但是如果我们的订单ID是一个标识符,我们只有在将其保存到数据库后才知道呢?在这种情况下,我们可能需要一些ID生成器。幸运的是,我们可以使用.NET Framework提供的机制更轻松地做到这一点。有一个名为CorrelationManager的类,引用MSDN,它关联属于逻辑事务一部分的跟踪。它通过使用两个静态(或更确切地说是线程静态)属性来实现:LogicalOperationStack
和ActivityId
。我通常只使用ActivityId
来分组跟踪(但您也可以使用LogicalOperationStack
获得更好的粒度)。ActivityId
只是一个GUID
数字,它存在于当前线程上下文中,并且可以很容易地被任何TraceListener
访问。只需在方法开始时设置其值,并在方法结束时恢复先前的值即可
public int PlaceOrder(String username, String product, decimal cash)
{
Guid prevActivityId = Trace.CorrelationManager.ActivityId;
Trace.CorrelationManager.ActivityId = Guid.NewGuid();
...
Trace.CorrelationManager.ActivityId = prevActivityId;
return order.Id;
}
然后,您需要为您的应用程序启用跟踪(通过app/web.config),并选择“活动感知”的TraceListener
,例如XmlWriterTraceListener
或RollingXmlTraceListener
,SqlDatabaseTraceListener
(来自优秀的EssentialDiagnostics库)
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="conn" connectionString="server=localhost\SQLEXPRESS;
database=ordersdb;Trusted_Connection=true" providerName="System.Data.SqlClient" />
<add name="diagnosticsdb" connectionString="server=localhost\SQLEXPRESS;
database=diagnosticsdb;Trusted_Connection=true" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="OrderingSystem" switchValue="Verbose">
<listeners>
<add name="sqldatabase"
type="Essential.Diagnostics.SqlDatabaseTraceListener, Essential.Diagnostics"
initializeData="diagnosticsdb"
applicationName="OrderingSystem" />
</listeners>
</source>
</sources>
</system.diagnostics>
</configuration>
最后一步是将ActivityId
保存在订单实体中,从而将您的订单与其跟踪相关联
public int PlaceOrder(String username, String product, decimal cash)
{
Guid prevActivityId = Trace.CorrelationManager.ActivityId;
Guid activityId = Guid.NewGuid();
Trace.CorrelationManager.ActivityId = activityId;
// create an order record
var order = new Order {
Username = username,
Product = product,
Cash = cash,
CreatedOn = DateTime.UtcNow,
ActivityId = activityId
};
...
Trace.CorrelationManager.ActivityId = prevActivityId;
return order.Id;
}
如果我们想完全符合Microsoft的跟踪标准,我们的PlaceOrder
函数的开头和结尾应该如下所示
Guid prevActivityId = Trace.CorrelationManager.ActivityId;
Guid activityId = Guid.NewGuid();
logger.TraceTransfer(0, "Starting the PlaceOrder method", activityId);
Trace.CorrelationManager.ActivityId = activity;
logger.TraceEvent(TraceEventType.Start, 0, "start");
...
logger.TraceTransfer(_transferOutId, "Finishing method", prevActivityId);
logger.TraceEvent(TraceEventType.Stop, 0, "stop");
Trace.CorrelationManager.ActivityId = prevActivityId;
很繁琐,不是吗?幸运的是,我们可以使用ActivityScope
类,同样来自Essential.Diagnostics
库,它可以为我们生成此代码
public int PlaceOrder(String username, String product, decimal cash)
{
using (new ActivityScope(logger)) {
// create an order record
var order = new Order {
Username = username,
Product = product,
Cash = cash,
CreatedOn = DateTime.UtcNow,
ActivityId = Trace.CorrelationManager.ActivityId
};
....
}
}
在最后改进之后,如果我们的方法碰巧是WCF请求的一部分,它的跟踪将很好地包装到WCF跟踪模型中
我希望我鼓励您使用框架提供的跟踪方法。它们是高度可配置的,允许您动态添加/删除跟踪侦听器,而无需更改应用程序代码。如果您正在将日志记录到数据库,您可能希望将事件过滤到仅非常重要的来源(例如,我们案例中的OrderingSystem
)。当系统出现问题或您需要更详细的跟踪时,您可以随时添加System.Diagnostics.EventProviderTraceListener
,它使用ETW基础结构来保存跟踪。ETW以对应用程序性能的影响非常低而闻名,因此可以安全地用于生产系统上的短期详细跟踪。像往常一样,所有源代码都可以在我的Codeplex示例页面上下载。
历史
- 2012年6月1日:初始版本