诊断资源管理器






4.93/5 (40投票s)
一个.NET库和网站,允许开发人员公开和查看其.NET进程的任意诊断信息。
目录
** 全新功能
引言
Diagnostic Explorer 是一个 .NET 库和网站,它允许开发人员公开和查看其 .NET 进程的任意诊断信息。可以轻松地公开和查看 .NET 对象在内存中构造和持有的属性,以及像 log4net 这样的框架生成的日志信息。在过去的 5 年里,我一直在生产系统中使用和开发 Diagnostic Explorer,发现它是一个用于监控 .NET 进程和快速解决问题的宝贵工具。
背景
该项目始于 2005 年,当时我试图弄清楚我正在开发的一个 Windows 服务内部发生了什么。当一切正常时,该服务运行良好,但如果出现问题,就需要进行广泛的侦探工作来诊断问题。唯一能问 Windows 服务的问题是“它是否在运行?”。
存在各种日志框架,例如 log4net 和 Microsoft 的 Logging Application Block,并且性能计数器可用于公开某些类型数据的性能指标。我发现现有的技术很笨拙,所以我写了一个简单的网站,它使用 Remoting 接口从 Windows 服务中获取和显示一系列键/值属性。随着我意识到如何使这种技术更通用,并且经历了多种技术(Remoting、HTML、AJAX、WinForms、WPF,以及最终的 WCF/Silverlight),Diagnostic Explorer 应运而生。
工作原理
任何 .NET 进程都可以通过引用 DiagnosticExplorer.dll 程序集来公开诊断信息。该程序集包含使您的应用程序出现在上面截图的 Silverlight Viewer 中所需的所有内容,并公开您想要的任何托管 .NET 对象的属性。
首先,您的应用程序使用 DiagnosticHostingService
类来托管一个 WCF 服务,您可以从该服务请求诊断属性、值和日志消息。该服务可以配置为使用特定端口,或使用随机的可用端口号(请参阅下面的 *公开诊断信息*)。
其次,您的应用程序使用 DiagnosticManager
类来注册将公开其属性的 .NET 对象,并使用 EventSink
类或日志框架(请参阅下面的 *使用 log4net*)来公开日志消息。
当您在 Silverlight Viewer 中选择您的应用程序时,您的应用程序托管的 WCF 服务将每秒轮询一次,以检索和显示诊断信息。
注册
Diagnostic Web Service 维护着一个已注册应用程序的列表(实际上是一个文件夹结构)。当您的应用程序启动时,Diagnostic Web Service 如何知道它正在运行以及它的终结点地址是什么?有两种选择。
- Silverlight Viewer 中的右键单击上下文菜单允许您通过提供应用程序的名称和终结点地址来手动注册应用程序。但要实现这一点,应用程序必须使用固定的端口号。
DiagnosticHostingService
可以配置为将其自己的名称和终结点地址注册到 Diagnostic Web Service。如果使用随机端口号,则此选项是强制性的。
公开诊断信息
只需两行代码(加上少量配置)即可开始在您自己的应用程序中公开诊断信息。您必须首先引用 DiagnosticExplorer.dll 并使用 DiagnosticHostingService.Start()
和 DiagnosticHostingService.Stop()
方法。此类负责托管 WCF 服务并向 Diagnostic Web Service 注册。
using DiagnosticExplorer;
...
public Form1()
{
InitializeComponent();
DiagnosticHostingService.Start();
FormClosed += StopDiagnostics;
}
private void StopDiagnostics(object sender, EventArgs e)
{
DiagnosticHostingService.Stop();
}
如果 DiagnosticHostingService
要自行注册,它必须知道如何联系 Diagnostic Web Service。
<configuration>
<system.serviceModel>
<client>
<endpoint address=
"https:///Diagnostics/RegistrationService.svc"
binding="wsHttpBinding"
bindingConfiguration= "WSHttpBinding_IRegistrationService"
contract="DiagnosticExplorer.IRegistrationService"
name="WSHttpBinding_IRegistrationService" />
</client>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IRegistrationService">
<security mode="Message" />
</binding>
</wsHttpBinding>
</bindings>
</system.serviceModel>
</configuration>
可以使用可选的配置部分来自定义 DiagnosticHostingService
的行为。
<configuration>
<configSections>
<section name="diagnosticExplorer"
type="DiagnosticExplorer.DiagnosticSectionHandler,
DiagnosticExplorer" />
</configSections>
<diagnosticExplorer>
<!-- Used to differentiate between multiple instances -->
<!-- of the same application running on a machine (optional) -->
<instanceName>Dev</instanceName>
<!-- Auto or Manual port number selection -->
<portMode>Auto</portMode>
<!-- Port to use when portMode is Manual -->
<port>12345</port>
<!-- True if this application should register
itself with the diagnostics web service -->
<autoRegister>true</autoRegister>
</diagnosticExplorer>
</configuration>
当您运行应用程序并且 DiagnosticHostingService
已启动后,Silverlight Viewer 中将已显示一些有限的系统信息。
公开诊断信息(Web应用程序)
在从 IIS 中托管的 Web 应用程序公开诊断信息时,可以在 Global.asax 的 Application_Start
和 Application_End
方法中使用 DiagnosticHostingService
,或者您可以向应用程序添加一个 WCF 服务并手动注册 URL。
- 将 Diagnostic Explorer 的 Web 服务(下载中的 DiagnosticWeb 文件夹)中的 WebDiagnostics.svc 文件复制到您自己的 Web 应用程序中(没有代码隐藏文件)。
- 从 web.config 中的
<system.serviceModel>
部分复制服务的 WCF 配置。您需要 <services><service name="WebDiagnostics" ...
<behaviors><serviceBehaviors><behavior name="WebDiagnostics" ...
<bindings><wsHttpBinding><binding name="WebDiagnostics" ...
- 通过在浏览器中输入新服务的 URL 来检查您是否可以访问它;例如:https:///MyWebApp/WebDiagnostics.svc。
- 在 Diagnostic Explorer Silverlight Viewer 中,右键单击左侧面板,然后选择“注册进程”。输入您的 WebDiagnostics 服务的名称和 URL。
公开属性
使用 DiagnosticManager.Register(obj, name, category)
方法轻松开始公开您自己的信息。只需选择一个您认为具有有趣属性的对象并注册它。
public partial class Form1 : Form
{
private SomeClass myObject;
public Form1()
{
InitializeComponent();
DiagnosticHostingService.Start();
FormClosed += (sender, e) => DiagnosticHostingService.Stop();
myObject = new SomeClass();
DiagnosticManager.Register(myObject,
"My Object", "My Category");
}
class SomeClass
{
public string MyProp1 { get { return "A Value"; } }
public string MyProp2 { get { return "A Different Value"; } }
}
}
运行应用程序后,Silverlight Viewer 现在应该如下所示。
请注意,必须维护对通过 DiagnosticManager
注册的任何对象的引用。这是因为 DiagnosticManager
只持有对您的对象的 WeakReference
,因此不会阻止垃圾回收。这有助于避免可能发生的内存泄漏,并消除了调用 DiagnosticManager.Unregister(myClass)
的需要。相反,您的已注册对象将在被垃圾回收后立即从 Silverlight Viewer 中消失。
公开可编辑属性
在任何属性上方添加 [Property(AllowSet = true)]
属性,它就可以从 Diagnostic Explorer 进行编辑。或者,您可以通过在类声明上方应用 [DiagnosticClass(AllPropertiesSettable = true)]
属性来指定类中的所有属性都可编辑。可编辑属性用蓝色前景表示。
Diagnostic Explorer 将尝试使用 Convert.ChangeType
设置属性。如果失败并且属性类型上存在静态 Parse(string)
方法,则会调用该方法。如果仍然失败,则会向用户显示错误消息。
[Property(AllowSet = true)]
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
公开方法
在任何在诊断中可见的对象的方法上方放置 [DiagnosticMethod]
,旁边就会显示一个黄色的星号。现在您可以单击并调用任何已注册的方法。
[DiagnosticMethod]
public string SayHelloSync(string caption, string message)
{
if (message == "throw")
throw new ArgumentException("Ok, I'll throw");
Stopwatch watch = Stopwatch.StartNew();
Action sayHello = () => MessageBox.Show(this, message, caption,
MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
Invoke(sayHello);
return string.Format("User clicked Ok in {0:N1} seconds", watch.Elapsed.TotalSeconds);
}
公开事件
有两种方法可以通过 Silverlight Viewer 公开事件。
使用 EventSink
第一种方法是从您的代码中直接使用 EventSink
类。如果您将以下代码添加到上面所示的 Form1
中,Silverlight Viewer 现在应该如下所示。
private EventSink mySink;
...
mySink = EventSink.GetSink("My Events", "My Category");
mySink.LogEvent(EventSeverity.Low, "Low severity issue",
"Low severity detail");
mySink.LogEvent(EventSeverity.Medium, "Medium severity issue",
"Medium severity detail");
mySink.LogEvent(EventSeverity.High, "High severity issue",
"High severity detail");
使用 log4net
然而,首选且更灵活的方法是使用日志框架,例如 log4net。
using log4net;
...
ILog log = LogManager.GetLogger(typeof(Form1));
log.Info("This is an INFO level log message");
log.Warn("This is a WARN level log message", myException);
log.Error("This is an ERROR level log message\n", myException);
在 log4net 配置文件中,您必须添加一个类型为 DiagnosticExplorer.Log4Net.DiagnosticAppender
的 appender,并使用所需的 SinkName
和 SinkCategory
进行配置。此 appender 在 log4net 框架指示记录事件时将使用 EventSink
类。
<appender name="DiagnosticExplorer_Form1"
type="DiagnosticExplorer.Log4Net.DiagnosticAppender, DiagnosticExplorer">
<SinkName>My Events</SinkName>
<SinkCategory>My Category</SinkCategory>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message" />
</layout>
</appender>
安全???
我意识到允许人们编辑(可能是在生产环境中)公开诊断信息的进程的属性和调用方法存在严重的安全性问题。我将在我的下一个版本中解决这个巨大的安全漏洞。
设置
在运行 Diagnostic Explorer 之前,请确保您已安装 Silverlight 4 Tools。
创建诊断Web服务
- 在 IIS 中创建一个名为 *Diagnostics* 的 Web 应用程序,该应用程序基于示例下载中的 DiagnosticWeb 文件夹。如果您正在处理源代码,这就是解决方案中的 DiagnosticExplorer.Web 项目。
- 检查您是否可以浏览到(假定为 localhost)
- https:///Diagnostics/RegistrationService.svc
- https:///Diagnostics/Diagnostics.svc
- https:///Diagnostics/WebDiagnostics.svc
- 检查您是否可以通过浏览到以下地址来运行 Silverlight Viewer:
- https:///Diagnostics/Default.aspx
配置WidgetSample
- 编辑提供的 WidgetSample 的 App.config 文件。在示例下载中,它是 WidgetSample\WidgetSample.exe.config;否则,就是 WidgetSample 项目。确保注册服务的客户端终结点地址正确。
- 运行 WidgetSample。
- 查看 Silverlight Viewer,验证示例应用程序是否出现。如果未出现,请运行 DebugView 查看是否报告了任何错误,然后进行故障排除。
设置“Retro Diagnostics”(可选)
这部分比前两个步骤要困难一些。我们想要实现的是日志事件的存储,以便它们可以在 Silverlight Viewer 的 *Retro* 选项卡中检索和显示。
可以编写一个 log4net appender,将日志事件直接插入数据库。但是,我不想让我的 appender 写入数据库;因此,我有一个 appender,它通过 MSMQ 将 WCF 服务写入。然后,一个 Windows 服务使用 netMsmqBinding 从队列中读取消息,并将它们插入数据库,然后 Diagnostic Web Service 可以从数据库中读取它们。
这听起来对于示例应用程序来说有太多的失败点,但我们开始吧。
创建数据库和消息队列
- 创建一个名为 Diagnostics 的数据库。脚本 CreateDatabase.sql 包含在解决方案和示例下载中。
- 创建一个名为 Diagnostics 的事务性消息队列。确保它具有适当的权限,以便 sample app 中的 log4net appender 可以写入,并且下一步中的 Windows 服务可以读取。
安装 Windows 服务
- 使用 installutil.exe 安装 Diagnostic Explorer Windows 服务。
- 确保 Windows 服务的配置文件包含正确的数据库连接字符串、
<system.serviceModel>
部分中的正确 MSMQ 路径,并且 RegistrationService 终结点是正确的。 - 从服务控制面板启动 Diagnostic Explorer 服务。检查 Windows 服务是否已在 Diagnostics 中使用 Silverlight Viewer 进行了注册。
配置和运行 WidgetSample
- 检查 WidgetSample 中的 log4net.xml 配置文件。确保
<appender-ref ref="MsmqLogger" />
没有被注释掉。它被配置为使用 LoggingService_MSMQ 终结点,因此请验证 App.config 中 LoggingService_MSMQ 终结点是否具有正确的队列路径。 - 运行 WidgetSample 并验证它是否出现在 Silverlight Viewer 中。通过单击一些按钮来生成一些事件。
- 验证 Windows 服务是否成功读取了消息队列中的事件。您可以通过查询数据库或在 Silverlight Viewer 中查看 Windows 服务来完成此操作。
查看“Retro”诊断
- 确保 Web 应用程序中的 Web.config 文件中的数据库连接字符串是正确的。
- 在 Silverlight Viewer 中选择 Retro 选项卡,然后单击 Refresh 按钮。如果一切正常,您应该会看到已记录到数据库中的事件。
请注意,如果您想提供自己的事件持久化和检索方式,您可以实现 DiagnosticExplorer.dll 中的 ILogWriter
和 ILogReader
。将您自己的实现放在 Windows 服务和 Web 应用程序的 bin 目录中,并相应地修改 App.config 和 Web.config 文件。
<!--
<add key="ILogReader" value="DiagnosticExplorer.SqlLoggingService,
DiagnosticExplorer.Common" />
<add key="ILogWriter" value="DiagnosticExplorer.SqlLoggingService,
DiagnosticExplorer.Common" />
-->
<add key="ILogReader" value="MyLogReader, MyAssembly" />
<add key="ILogWriter" value="MyLogWriter, MyAssembly" />
高级功能
公开静态属性
如果您有一个类包含一些有趣的静态属性,您可以像注册对象实例一样注册它。
DiagnosticManager.Register(typeof(MyClass),
"Static Stuff", "My Category");
...
public class MyClass
{
public static string Hello { get { return "World"; } }
public static string Goodbye { get { return "Cruel World"; } }
}
阻止属性在诊断中公开
要阻止特定属性在诊断中公开,请使用 PropertyAttribute
并设置 Ignore = true
。或者,使用 [Browsable(false)]
属性。
class SomeClass
{
public string MyProp1 { get { return "I'm not ignored"; } }
//Won't be visible in diagnostics
[Property(Ignore = true)]
public string MyProp2 { get { return "I am ignored"; } }
}
仅公开特定属性在诊断中显示
通过使用 DiagnosticClassAttribute
,您可以指定只有显式标记为 PropertyAttribute
或 [Browsable(true)]
的属性才会在诊断中公开。
[DiagnosticClass(AttributedPropertiesOnly = true)]
class SomeClass
{
[Property]
public string MyProp1 { get { return "I am attributed :)"; } }
//Won't be visible in diagnostics
public string MyProp2 { get { return "I'm not attributed :("; } }
}
防止显示继承的属性
如果您的诊断类是子类,并且您不希望显示任何继承的属性,请使用 [DiagnosticClass(DeclaringTypeOnly = true)]
属性。请注意,如果超类也使用此属性,那么它的属性 *将* 被子类公开。
public class Class1
{
[Browsable(true)]
public string PropA
{ get { return "I'm not exposed despite being browsable"; } }
}
[DiagnosticClass(AttributedPropertiesOnly = true, DeclaringTypeOnly = true)]
public class Class2 : Class1
{
[Browsable(true)]
public string PropB { get { return "I am exposed"; } }
[Property]
public string PropC { get { return "I am exposed too"; } }
public string PropD { get { return "I'm not exposed (no attribute)"; } }
}
自定义属性的显示方式
PropertyAttribute
可用于自定义属性的公开方式。
class SomeClass
{
public string MyProp1 { get { return "A Value"; } }
[Property(Name = "Alternate name",
Category = "Some Category",
Description = "This appears in the tooltip",
FormatString = "Value is: {0:N3}")]
public double MyIntValue { get { return 12345.6789; } }
}
请注意,ComponentModel
属性 [Category("Some Category")]
和 [Description("This appears in the tooltip")]
也可用于此目的。
在自己的类别中公开复杂属性
将对象属性转换为字符串的默认行为是使用 Convert.ToString
或 string.Format
(如果指定了格式字符串)。对于本身就是复杂对象的属性,此行为可以通过 ExtendedPropertyAttribute
进行修改。
class SomeClass
{
public string MyProp1 { get { return "A Value"; } }
[ExtendedProperty]
public Person SomePerson{ get; set; }
}
公开集合
集合属性可以在诊断中显示。默认情况下,下面的代码在 Diagnostics 中看起来像这样。
class SomeClass
{
public string MyProp1 { get { return "A Value"; } }
//Contains 12 items
public IList<Person> People { get { return _myListOfPeople; } }
}
class Person
{
public string Name { get; set; }
public int Height { get; set; }
public string Nationality { get; set; }
public override string ToString()
{
return string.Format("{0}, {1}cm, {2}", Name, Height, Nationality);
}
}
将 CollectionPropertyAttribute
应用于集合属性可用于自定义集合的显示方式。单击下面的链接以了解该属性如何影响行为。
[CollectionProperty(CollectionMode.Count)]
[CollectionProperty(CollectionMode.Count, FormatString="There are {0:N2} people")]
[CollectionProperty(CollectionMode.List)]
[CollectionProperty(CollectionMode.Concatenate, Separator = " --> ", MaxItems = 3)]
[CollectionProperty(CollectionMode.Concatenate, Separator = " --> ", MaxItems=2, FormatString="Person({0})")]
[CollectionProperty(CollectionMode.List, Category="My People", NameProperty="Name")]
[CollectionProperty(CollectionMode.Categories)]
[CollectionProperty(CollectionMode.Categories, CategoryProperty = "Name", Category="People")]
公开速率
为了帮助您在代码中捕获事件速率,我们提供了 RateCounter
类。它类似于轻量级的性能计数器,它向 Diagnostics 公开每秒接收信号数的平均值。
下面的代码在 Diagnostics 中查看时如下所示。
如果您取消注释 RateProperty
属性,它将显示如下。
class SomeClass
{
private System.Timers.Timer _myTimer = new System.Timers.Timer(100);
private Random _rand = new Random();
public SomeClass()
{
//Construct a rate counter which reports average over 5 seconds
ThingsHappened = new RateCounter(5);
_myTimer.Elapsed += (sender, e) => ThingsHappened.Register
(_rand.Next(1, 100));
_myTimer.Start();
}
public string MyProp1 { get { return "A Value"; } }
//[RateProperty(ExposeRate = true, ExposeTotal = true)]
public RateCounter ThingsHappened { get; private set; }
}
TraceScope
在远程排除故障时,了解代码的性能会很有帮助:调用了哪些方法,它们的顺序如何,发生了哪些重要事件,以及所有这些花费了多少时间。您可以记录事件并检查结果,但这在同时进行多个操作时尤其困难。也许有多个线程都在运行相同的代码,您必须设法确定哪些日志消息属于哪个线程。
为了解决这个问题,我在 DiagnosticExplorer.dll 中包含了一个名为 TraceScope
的类。它是一个类,它建立了一个线程静态的“环境”范围,该范围收集跟踪信息。它应该始终在一个 using
块中构造,如以下所示,并且可以向构造函数传递一个 Action<string>
。当 TraceScope
被释放时,它会创建一个收集到的跟踪信息的字符串表示,并将其传递给 Action<string>
。
您可以从任何程序集中的任何位置调用静态方法 TraceScope.Trace("My debug message")
,如果存在环境 TraceScope
,该消息将被追加到其中。如果您创建一个 TraceScope
并且已经有一个环境 TraceScope
,那么这些范围将被嵌套。
考虑下面的代码,并查看引用的 TraceScopeExample
类。 请参见此处。在 TraceScope
构造函数中,我传递了一个 log4net 日志记录器的 ILog.Info(string)
方法,这意味着当作用域被释放时,_formLog.Info
方法将被调用,并返回 TraceScope
的结果,您可以在 此处 看到。
private static ILog _formLog = LogManager.GetLogger(typeof (Form1));
private void btnTraceScope_Click(object sender, EventArgs e)
{
using (new TraceScope(_formLog.Info))
{
TraceScope.Trace("In Trace Scope Button Click");
TraceScopeExample.TestTraceScope1();
}
MessageBox.Show("Just generated a trace scope. Check diagnostics.");
}
TraceScope
的输出是代码在各种范围内的路径以及收集到的所有跟踪消息的缩进文本表示。您在方括号中看到的数字是自最外层作用域创建以来的总时间以及自最后一条跟踪消息以来的经过时间。
Silverlight Viewer 会识别出日志消息何时包含 TraceScope
的结果。它会对其进行解析并提供用户友好的显示,并尝试突出显示花费时间最多的执行路径。您可以在查看器中看到它的样子。
我认为可以改进它,所以欢迎任何想法。
自定义行为
web.config 文件包含一些可自定义的设置。
<appSettings>
<-- Time after which Auto registered processes renew their registrations -->
<add key="RenewTime" value="00:00:20" />
<-- The frequency (ms) at which the Silverlight Viewer polls a process -->
<add key="PollTime" value="1000" />
<-- The frequency (ms) at which the Silverlight Viewer
refreshes the process menu -->
<add key="MenuRefresh" value="2000" />
<!-- The implementations of ILogReader and ILogWriter
which are used to read and write "retro" events -->
<add key="ILogReader" value="DiagnosticExplorer.SqlLoggingService,
DiagnosticExplorer.Common" />
<add key="ILogWriter" value="DiagnosticExplorer.SqlLoggingService,
DiagnosticExplorer.Common" />
</appSettings>
其中两项设置 PollTime
和 MenuRefresh
可以在 URL https:///Diagnostics/Default.aspx?PollTime=1500&MenuRefresh=5000 中覆盖。
评论和待办事项
虽然我提到这个系统在生产环境中使用,但这里提供的版本是第一个使用 WCF、自动端口选择和 Web 服务自注册的版本。在之前的版本中,.NET Remoting 用于通信,端口选择是手动的,您必须手动更新 XML 文件来注册应用程序。这有一个缺点,就是无法启动同一进程的多个实例,因为端口会被第一个启动的进程占用。
在 Retro 选项卡中存在一个小 bug,如果您单击了网格中的某个项目,则 Refresh 按钮将不起作用。
源代码托管在 http://sourceforge.net/projects/diagexplorer/。如果您对此感兴趣并有一些想法,我很欢迎贡献者。
历史
- 2010年3月4日
- 初始版本。
- 2010年3月14日:进行了一些更改,以消除日志消息对 MSMQ 的显式使用。
- 删除了
MessageQueueAppender
log4net appender。它已被LoggingServiceAppender
取代,后者使用 WCF 上的ILogWriter
服务,因此您现在可以使用任何您想要的传输方式在您的应用程序和日志服务之间进行通信。 - Retro 功能必须创建的 Message Queue 现在必须是*事务性*队列。
IEventIO
接口已被ILogReader
和ILogWriter
取代。- 2010年3月28日
- 转换为 Visual Studio 2010 和 Silverlight 4 RC。
- 增加了对
ComponentModel
属性的支持:BrowsableAttribute
、CategoryAttribute
和DescriptionAttribute
。 - 2010年10月24日
- 转换为 Silverlight Toolkit Apr. 2010。
- 改进了上下文菜单。
- 改进了重命名和新建注册弹出窗口。
- 用
new TransactionScope(TransactionScopeOption.Suppress)
包装了LoggingServiceAppender
的Send
方法,以停止对事务的干扰。 - 修复了几个小的 UI 错误。
- 2010年11月7日
- 改进了在 Web 应用程序中公开诊断信息支持和文档。
- 禁用了左侧树中的拖放功能,因为 Silverlight Toolkit 树会随机启动拖动操作。(通过上下文菜单重新启用它。)
- 重新压缩了所有图像,希望它们不会被 CodeProject 缩放。
- 2010年11月27日
- 您现在可以从 Diagnostic Explorer 编辑属性和调用方法了!
- 右侧的选项卡在事件到达时会改变颜色。