65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2012年6月1日

CPOL

4分钟阅读

viewsIcon

29730

我不需要强调跟踪(日志记录)在任何应用程序中的重要性。没有日志,我们常常无法诊断故障原因。日志还有助于我们跟踪应用程序随时间的行为和使用情况。

引言

我不需要强调跟踪(日志记录)在任何应用程序中的重要性。没有日志,我们常常无法诊断故障原因。日志还有助于我们跟踪应用程序随时间的行为和使用情况。幸运的是,有很多优秀的库(包括.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,它关联属于逻辑事务一部分的跟踪。它通过使用两个静态(或更确切地说是线程静态)属性来实现:LogicalOperationStackActivityId。我通常只使用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,例如XmlWriterTraceListenerRollingXmlTraceListenerSqlDatabaseTraceListener(来自优秀的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日:初始版本
© . All rights reserved.