Clog:客户端日志记录,Silverlight 版本






4.84/5 (50投票s)
一个可定制的日志提供程序系统,允许您利用现有的日志系统将客户端消息记录到服务器。包含一个 Silverlight 界面和日志查看器。
请访问 CodePlex 项目站点获取最新版本和源代码。
目录
- 引言
- 背景
- Clog 系统概述
- 使用 Clog
- 从 Silverlight 写入日志
- Silverlight 日志查看器
- Silverlight 安全模型
- 扩展 Clog
- 以 Clog 方式记录异常
- 日志条目
- 未来的增强
- 结论
- 参考文献
- 历史
引言
假设您已经将全新的 Silverlight 应用程序部署到测试环境。但是等等,出问题了!测试人员告诉您,当他们执行某个特定操作时,程序会崩溃。他们给您发了大量的截图,但都无济于事。没有更多信息,这个问题似乎无法解决。随着截止日期的临近,希望渺茫,绝望感油然而生。您高举双拳,大声喊道:“要是我能知道客户端发生了什么就好了!”
Clog 应运而生。
好吧,前面那个假设性场景有点夸张,但它凸显了对集成式客户端日志记录解决方案的需求。因此,我决定创建 Clog。Clog 是一个日志提供程序系统,允许您利用现有的日志系统将客户端消息记录到服务器。它是完全可定制的,可以序列化和记录所有 Exception
类型,并允许使用自定义或内置的筛选器在客户端和服务器端筛选消息,目前内置的筛选器包括 IP 地址范围筛选器和角色成员筛选器。在此版本中,Clog 包含一个可扩展的日志提供程序系统、一个 log4net 日志策略、一个 Microsoft Enterprise Library 日志策略,以及一个 Silverlight 日志查看器。Clog 现在支持多个日志策略同时运行!
本文将讨论 Clog 的工作原理、如何在客户端和服务器上设置 Clog、Silverlight 日志查看器控件的配置,以及一些更高级的主题,如 Silverlight 安全模型。
尽管本文的客户端重点主要在于 Silverlight 实现,但 Clog 能够为任何 .NET 或 Web 服务消费者提供日志记录服务。
背景
一个稳固的服务器端日志系统是大多数基于 Web 的应用程序的支柱。随着 WPF、Silverlight 和客户端 CLR 的出现,我认为我们的关注点将从传统的、主要基于服务器的日志记录场景,转向满足更加以客户端为中心的环境。
虽然 Visual Studio 让我们能够轻松调试 Silverlight、Windows Forms 和 WPF 应用程序,但如果没有调试器或某种跟踪客户端事件的方法,我们可能会一无所知。我们没有内置的机制来记录到客户端机器上的事件日志等,也缺乏一个即时的反馈机制,让我们知道我们的客户端 .NET 应用程序是否行为正常。Clog 弥合了这种客户端与服务器之间的鸿沟。我们现在能够选择性地捕获源自客户端和服务器端应用程序的日志事件。
我记得几年前,我第一次手动编写表单验证 JavaScript,并试图为自己提供客户端反馈。当时,使用消息框来显示信息是一种常见的做法,现在仍然如此。这是一种相当草率和随意的方法,而且如果您在完成后忘记注释掉代码,还可能会很尴尬!如今,有一两个 AJAX JavaScript 客户端到服务器的日志记录项目。虽然它们可能在弱类型 JavaScript 中工作得很好,但它们解决了不同的需求。
有许多用于 .NET 的日志记录库,我们中的许多人随着时间的推移已经了解并依赖于某个特定的系统;有时还会根据我们自己的需求对其进行定制。Clog 允许我们保留现有的系统,通过包装它,使我们能够以相同的方式执行客户端和服务器端的日志记录。
Clog 系统概述
Clog 的核心组件是 DanielVaughan.Logging.dll。它提供了大部分服务器端功能。Silverlight 日志记录功能位于 DanielVaughan.Logging.Silverlight.dll 中,作为其辅助的是可选组件 DanielVaughan.Logging.Silverlight.UI.dll,其中包含 LogViewer
Silverlight 控件。下图提供了 Clog 的一些主要类型及其相互依赖关系的概念性概述。
使用 Clog
要为客户端日志记录启用 Clog,我们需要完成一个两步过程。首先,我们配置基于服务器的项目以使用 Clog。然后,我们配置客户端项目以使用 Clog。
服务器端配置
要在服务器端启用 Clog,请添加对 DanielVaughan.Logging 程序集的引用。
<section name="Clog"
type="DanielVaughan.Logging.Configuration.ClientLoggingConfigurationSectionHandler,
DanielVaughan.Logging"/>
接下来,创建 Clog
配置节,如以下摘录所示
<!-- InternalLogLevel is used to monitor log messages originating from Clog,
and which are written to the console. Valid values are (from less to most restrictive):
All, Debug, Info, Warn, Error, Fatal, None.
Xmlns is specified in order to gain intellisense within the Visual Studio config editor.
Place the Clog schema located in the project\Schemas directory
into C:\Program Files\Microsoft Visual Studio 9.0\Xml\Schemas directory.
SkipFrameCount is used to specify the number of frames to skip when resolving
the calling method of a write log call. Defaults to 4 if absent. -->
<Clog xmlns="http://danielvaughan.orpius.com/Clog/2/0/"
InternalLogLevel="All" SkipFrameCount="4">
<LogStrategy Name="Simple"
Type="ExampleWebsite.SimpleLogStrategy, ExampleWebsite">
<Filter Name="IPAddressRange"
Type="DanielVaughan.Logging.Filters.IPAddressRangeFilter,
DanielVaughan.Logging"
Begin="127.0.0.0" End="127.0.0.10"/>
</LogStrategy>
<LogStrategy Name="Log4Net"
Type="DanielVaughan.Logging.LogStrategies.Log4NetStrategy,
DanielVaughan.Logging.Log4NetLogStrategy">
<Filter Name="IPAddressRange"
Type="DanielVaughan.Logging.Filters.IPAddressRangeFilter,
DanielVaughan.Logging"
Begin="127.0.0.0" End="127.0.0.10"/>
<!-- Uncomment to prevent access to those users that
do now have membership of the specified roles. -->
<!--
<Filter Name="RoleMembership"
type="DanielVaughan.Logging.Filters.RoleMembershipFilter,
DanielVaughan.Logging"
Roles="Developer, Administrator" />
-->
</LogStrategy>
</Clog>
在您的 Web 项目中创建一个名为 ClogService.svc 的新文件,打开它,并粘贴以下内容
<%@ ServiceHost Language="C#" Debug="true" Service="DanielVaughan.Logging.ClogService" %>
服务代码实际上位于 DanielVaughan.Logging.dll 程序集中。您需要自己定义偏好的日志记录方法。在本演示中,我们使用 log4net。虽然配置 log4net 超出了本文的范围,但您可以查看示例网站的下载文件,了解其具体做法。简而言之,它需要添加对 log4net.dll 的程序集引用,在 web.config 中添加一个配置节,然后在您的网站中初始化 log4net。我通过在应用程序启动时执行一个任意的日志记录请求来完成此操作。
注意:如果您在另一个程序集使用 log4net 之前,没有从您的网站初始化它,它将不会被正确配置。
您可能会注意到配置中新增的 SkipFrameCount
属性。这是自 1.8 版本以来的新功能,它允许您将 Clog 包装在您自己的适配器中,同时仍然能够正确解析日志写入调用的来源。
Clog for Silverlight 配置
要在您的 Silverlight 项目中使用 Clog,请添加对 DanielVaughan.Logging.Silverlight 程序集的引用。如果您还希望使用 Silverlight 日志查看器,那么也请添加对 DanielVaughan.Logging.Silverlight.UI 程序集的引用。
从 Silverlight 写入日志
要在客户端,即 Silverlight 应用程序中使用 Clog,我们添加对 DanielVaughan.Logging.Silverlight 程序集的引用,并使用 LogManager
通过调用 GetLog
方法来为我们提供一个 ILog
,如下所示
static readonly ILog log = LogManager.GetLog(typeof(Page));
typeof(Page)
参数与页面本身的 URL 结合使用,以确定日志的名称。这使我们能够对日志请求的筛选进行精细控制。也就是说,我们不仅可以基于特定的 Silverlight 自定义控件或页面来控制日志记录请求,还可以根据其部署位置进行控制。为了方便从 localhost 进行调试,其中 URL 中指定了动态端口,我们不在日志名称中包含端口号。
客户端 Silverlight 日志异步地将日志条目分派到服务器,如下面的摘录所示
void WriteLogEntryAux(LogLevel logLevel, string message,
Exception exception, IDictionary<string, object> properties)
{
ExceptionMemento memento = null;
if (exception != null)
{
memento = CreateMemento(exception);
}
var logEntryData = new LogEntryData
{
LogLevel = logLevel,
Message = message,
ExceptionMemento = memento,
CodeLocation = GetLocation(),
LogName = Name,
ThreadName = Thread.CurrentThread.Name,
ManagedThreadId = Thread.CurrentThread.ManagedThreadId,
Url = pageUri.ToString(),
OccuredAt = DateTime.Now,
Properties = properties != null ?
new Dictionary<string, object>(properties) : null
};
OnLogEntrySendAttempt(new LogEventArgs(logEntryData));
var clientInfo = new ClientInfo
{
LogName = logEntryData.LogName,
MachineName = logEntryData.MachineName,
Url = logEntryData.Url,
UserName = logEntryData.UserName
};
if (clientConfigurationData == null ||
clientConfigurationData.RetrievedOn.AddSeconds(
clientConfigurationData.ExpiresInSeconds) < DateTime.Now)
{
lock (loggingConfigLock)
{
if (clientConfigurationData == null ||
clientConfigurationData.RetrievedOn.AddSeconds(
clientConfigurationData.ExpiresInSeconds) < DateTime.Now)
{
var clogService = GetClogService();
clogService.BeginGetConfiguration(clientInfo,
asyncResult =>
{
ClientConfigurationData result;
try
{
result = clogService.EndGetConfiguration(asyncResult);
}
catch (Exception ex)
{
OnInternalMessage(
new InternalMessageEventArgs(
"Unable to retrieve configuration from server.", ex));
return;
}
try
{
clientConfigurationData = result;
clientConfigurationData.RetrievedOn = DateTime.Now;
WriteLogEntryAux(logEntryData, clientConfigurationData);
}
catch (Exception ex)
{
OnInternalMessage(
new InternalMessageEventArgs(
"Problem writing log entry after successfully retrieving configuration.",
/* TODO: Make localizable resource. */
ex));
throw;
}
}, null);
return;
}
}
}
WriteLogEntryAux(logEntryData, clientConfigurationData);
}
您可能会注意到,我们在前面的摘录中使用了一个名为 ChannelManagerSingleton
的类。关于它的一些信息可以在我的另一篇文章中找到。它主要用于缓存服务通道。
Silverlight 日志查看器
概述
日志查看器是一个 Silverlight 控件,可以放置在 Canvas
上以自动监视 LogManager
。当一个 ILog
实例收到写入日志消息的请求时,日志查看器能够显示该日志消息。以下屏幕截图显示了日志查看器正在接收一个传出的日志条目,而 Log4Net 查看器则显示了该日志条目被中继到 log4net 后的结果。
使用日志查看器
要将日志查看器包含在您的 Silverlight 应用程序中,请添加对 DanielVaughan.Logging.Silverlight.UI 程序集的引用,并将以下命名空间定义添加到页面的根 canvas 中
xmlns:UI="clr-namespace:DanielVaughan.Logging.Silverlight.UI;
assembly=DanielVaughan.Logging.Silverlight.UI"
然后,将 LogViewer
XAML 元素放置在页面的某个位置。
<UI:LogViewer x:Name="LogViewer" />
此日志查看器已在 Silverlight 2 RTW 版本中重新构建,坦率地说,功能相当基础。
日志查看器内部
当 Silverlight 应用程序请求写入日志条目时,活动的 ILog
实例可能会引发两个事件。第一个事件 WriteRequested
无条件发生,如果“日志查看器”处于 OfflineMode
(离线模式),日志条目将立即显示。第二个事件 LogEntrySent
在日志条目发送到服务器时引发。在这种情况下,如果“日志查看器”不处于 OfflineMode
,则会显示该日志条目。是否发送日志条目取决于 ILog
的 ClientConfigurationData
中的 LogLevel
和 Enabled
属性,以及请求的日志级别。
Silverlight 安全模型
Silverlight 不使用代码访问安全 (CAS)。Silverlight 使用 .NET 2.0 中引入的透明度模型。在此模型中,有三个级别:透明 (Transparent)、安全关键 (Safe Critical) 和关键 (Critical)。在 Silverlight CLR 中,所有代码默认都是“透明的”,因此用户代码也是如此。这与桌面 CLR 相反,后者默认是“关键的”(.NET 安全博客)。任何使用 SecurityCritical
属性修饰的方法都不能由用户代码直接调用,因为透明代码不允许调用“关键”代码。如果“透明”代码试图调用“关键”代码,则会发生 MethodAccessException
异常。
Silverlight 的 mscorlib 包含“许多”用 SecurityCritical
属性修饰的方法。其中一个就是 System.Diagnostics.StackTrace
构造函数,对我们来说不幸的是,这意味着我们无法获取堆栈跟踪;这使我们无法获取和传递日志调用的来源。对用户代码隐藏堆栈跟踪信息是合理的,因为它可能泄露敏感信息。不过,如果能有一个“安全关键”的方法来检索仅包含用户代码的堆栈跟踪,那就太好了。但是,我对此不抱太大希望。
要查看哪些方法对我们的用户代码可用,请启动 Reflector,并将 Reflector 中的 Framework mscorlib 程序集替换为 Silverlight 版本。正如我们所见,这个程序集的很大部分都是禁区。
扩展 Clog
Clog 提供程序模型
“你所有的日志都属于 Clog。”
-D. Vaughan. (参见出处)
Clog 使用 ILogStrategy
实例将日志条目发送到第三方日志系统。Clog 附带了两个 ILogStrategy
实现。第一个是一个非常简单的跟踪策略,作为一个基本示例;第二个是一个 log4net 策略。我希望在后续版本中包含更多。如果您碰巧为某个特定的第三方日志系统编写了一个,我很乐意在下一个版本中包含它(当然会署名)。
ILogStrategyProvider
的任务是实例化 ILogStrategy
并通过其 LogStrategy
属性公开它。在 DanielVaughan.Logging
模块内部,有一个默认的 ILogStrategy
实现,在大多数情况下应该足够了。
将 Clog 与您现有的第三方日志系统集成
要将 Clog 与现有日志系统集成,请实现 ILogStrategy
接口,并在提供程序配置中使用 LogStrategy
属性指定类型,如以下示例所示
<LogStrategy Name="CustomStrategy" Type="YourAssembly.Strategy, YourAssembly">
<!-- Add filters here. -->
</LogStrategy>
ILogStrategy
接口有三个成员。对于客户端日志记录功能,请实现 void Write(IClientLogEntry logEntry);
和 LogLevel GetLogLevel(string logName);
方法。如果您计划仅将 Clog 用于客户端日志记录,那么可以忽略 void Write(IServerLogEntry logEntry);
重载(当然,您仍然需要一个默认的空实现)。但是,如果您希望将 Clog 用作现有日志系统的包装器,那么也请为 IServerLogEntry
重载提供适当的实现。
日志策略决定了日志条目如何写入日志。我们正是在这里将我们现有的日志系统(如 log4net)连接到 Clog。当请求写入日志时,当前的日志策略必须获取 IServerLogEntry
或 IClientLogEntry
中存在的信息,并构建对现有日志系统的调用。LogLevel GetLogLevel(string logName);
方法用于确定写入日志条目的阈值,并且如果级别高于请求的日志级别,它还允许我们抑制客户端的日志记录。
此版本的 Clog 附带一个简单的跟踪日志策略、一个 log4net 策略 (Log4NetStrategy
) 和一个 Microsoft Enterprise Library Logging Application Block 策略 (EnterpriseLibraryStrategy
)。以下摘录显示了该类如何使用指定的 IClientLogEntry
参数将日志消息写入 log4net
public void Write(IClientLogEntry logEntry)
{
ILog log = defaultLog;
if (logEntry.LogName != null)
{
log = LogManager.GetLogger(logEntry.LogName);
}
/* Create a Log4Net event data instance,
* and populate it with our log entry information. */
LoggingEventData data = new LoggingEventData();
if (logEntry.ExceptionMemento != null)
{ /* Use the exception memento to write
* the message and stack trace etc. */
data.ExceptionString = logEntry.ExceptionMemento.ToString();
}
data.Level = GetLog4NetLevel(logEntry.LogLevel);
ICodeLocation location = logEntry.CodeLocation;
if (location != null)
{
data.LocationInfo = new LocationInfo(location.ClassName,
location.MethodName, location.FileName,
location.LineNumber.ToString());
}
data.LoggerName = logEntry.LogName;
data.Message = string.Format("{0}\nMachineName:{1}",
logEntry.Message,
logEntry.MachineName);
data.ThreadName = logEntry.ThreadName;
data.TimeStamp = logEntry.OccuredAt;
data.UserName = logEntry.UserName;
/* Copy custom properties into log4net Properties. */
if (logEntry.Properties != null && logEntry.Properties.Count > 0)
{
var properties = new log4net.Util.PropertiesDictionary();
foreach (var prop in logEntry.Properties)
{
properties[prop.Key] = prop.Value;
}
data.Properties = properties;
}
LoggingEvent loggingEvent = new LoggingEvent(data);
log.Logger.Log(loggingEvent);
}
筛选器
Clog 使用服务器端筛选器来决定在将日志条目发送到活动的日志策略之前丢弃哪些条目。筛选器在检索 ClientConfigurationData
时以及在收到日志写入请求时进行评估。此版本中我包含了一些筛选器。一个是 IP 地址范围筛选器,它将日志记录限制在提供程序配置中定义的 IP 范围内。另一个是角色成员筛选器,它将日志记录限制为那些具有配置中指定角色的已验证用户。更新:自发布以来,我已在这篇文章中详细介绍了各种新的筛选器。
当前的 ILogProvider
通过调用 IsValid
方法来评估每个 IFilter
,如下面的 IPAddressFilter
类的摘录所示。
/// <summary>
/// Restricts logging based on an IP address range.
/// </summary>
class IPAddressRangeFilter : FilterBase
{
uint begin;
uint end;
public IPAddressRangeFilter()
{
Init += IPAddressRangeFilter_Init;
}
void IPAddressRangeFilter_Init(object sender, FilterInitEventArgs e)
{
ArgumentValidator.AssertNotNull(e, "e");
ArgumentValidator.AssertNotNull(e.ConfigurationElement,
"e.ConfigurationElement");
/* Reset state. */
begin = end = 0;
var beginAttribute = e.ConfigurationElement.Attributes["Begin"];
if (beginAttribute == null)
{ /* TODO: Make localizable resource. */
throw new ClientLoggingException("Begin ip address " +
"attribute does not exists.");
}
try
{
begin = ToUInt(IPAddress.Parse(beginAttribute.Value));
}
catch (Exception ex)
{ /* TODO: Make localizable resource. */
throw new ClientLoggingException("Begin ip address " +
"is not a valid IP address.", ex);
}
var endAttribute = e.ConfigurationElement.Attributes["End"];
if (endAttribute == null)
{ /* TODO: Make localizable resource. */
throw new ClientLoggingException("End ip address attribute does not exists.");
}
try
{
end = ToUInt(IPAddress.Parse(endAttribute.Value));
}
catch (Exception ex)
{ /* TODO: Make localizable resource. */
throw new ClientLoggingException("End ip address " +
"is not a valid IP address.", ex);
}
if (Action == FilterAction.Default)
{
Action = FilterAction.Allow;
}
if (Action != FilterAction.Allow && Action != FilterAction.Deny)
{
throw new ConfigurationErrorsException(InvalidActionMessage);
}
}
public override bool IsValid(LogEntryOrigin origin, IClientInfo info)
{
string addressValue = info.IPAddress;
bool withinRange = string.IsNullOrEmpty(addressValue) ||
IsWithinRange(begin, addressValue, end);
switch (Action)
{
case FilterAction.Allow:
return withinRange;
case FilterAction.Deny:
return !withinRange;
}
throw new ConfigurationErrorsException(InvalidActionMessage);
}
/// <summary>
/// Determines whether the specified addressValue IP address
/// falls within the range specified by beginAddress and endAddress.
/// </summary>
/// <param name="beginAddress">The begin address.</param>
/// <param name="addressValue">The address value to test.</param>
/// <param name="endAddress">The end address.</param>
/// <returns>
/// <c>true</c> if the addressValue is within
/// the specified range; otherwise, <c>false</c>.
/// </returns>
static bool IsWithinRange(uint beginAddress, string addressValue, uint endAddress)
{
IPAddress address;
if (!IPAddress.TryParse(addressValue, out address))
{
return false;
}
uint ip = ToUInt(address);
return ip >= beginAddress && ip <= endAddress;
}
/// <summary>
/// Converts and <see cref="IPAddress"/> to an unsigned int.
/// </summary>
/// <param name="ipAddress">The ip address to convert.</param>
/// <returns>A <code>uint</code> representing
/// the specified ipAddress.</returns>
static uint ToUInt(IPAddress ipAddress)
{
byte[] bytes = ipAddress.GetAddressBytes();
uint result = (uint)bytes[0] << 24;
result += (uint)bytes[1] << 16;
result += (uint)bytes[2] << 8;
result += bytes[3];
return result;
}
}
以 Clog 方式记录异常
当客户端发生记录 Exception
的请求时,会使用一个 ExceptionMemento
来封装异常信息,以便可以正确地序列化并通过网络发送。您可能知道,如果不使用二进制格式化程序进行序列化,Exception
是不能直接序列化的,这是由于其 IDictionary _data
字段。为了避开任何序列化问题,并使 Clog 的使用者不必担心其记录的异常的可序列化性,我们将异常中能获取到的信息收集到一个 ExceptionMemento
实例中。然后,通过 HTTP 将其发送到我们的 Clog Web 服务,在那里它被中继到活动的日志策略。
日志条目
ILogEntry
实例封装了通过远程客户端应用程序或本地消费者流经 Clog,并最终到达终点(即活动的 ILogStrategy
)的日志条目信息。Clog 的外部使用者使用 LogEntryData
数据类型,该类型是所有具体 ILogEntry
实例的基类,并省略了各种内部属性。当一个 LogEntry
到达 Clog Web 服务时,会实例化一个 ClientLogEntry
,并用传入请求的 IP 地址对其进行修饰。然后将其发送到静态的 Log
类。以下类图显示了 ILogEntry
接口及其继承者。各种 ILogEntry
接口的具体实现是 DanielVaughan.Logging 项目的内部实现。
IServerLogEntry
实例代表服务器端的日志条目,用于当写入日志的请求源自与 DanielVaughan.Logging.dll 组件在同一应用程序中,且很可能在同一应用程序域内时。
另一方面,IClientLogEntry
实例代表客户端的日志条目。当写入日志的请求源自远程客户端(例如 Silverlight Clog 日志)时使用这些实例。
未来的增强
- 使用自定义异常将受保护的数据暴露给
ExceptionMemento
- 向日志查看器添加按日志等筛选的功能
- 为核心日志记录功能提供更多单元测试
结论
本文讨论了 Clog 的实现,它是一个客户端-服务器日志记录提供程序系统。文章展示了如何进行设置,包括 Silverlight 日志查看器控件的配置。文章还涉及了一些更高级的主题,例如 Silverlight 安全模型。您可以在这里找到本系列的下一篇文章。虽然 Clog 仍处于起步阶段,但我相信它在成为一个相当有用的工具方面显示出很大的潜力。
希望您觉得这个项目有用。如果是,您可以对它进行评分和/或在下方留下反馈。
参考文献
- .NET 安全博客,2007,当透明的反面不是不透明
- .NET 安全博客,2007,Silverlight 安全 II:什么使方法变得关键
2007 年 11 月 21 日检索自 MSDN。
2007 年 11 月 21 日检索自 MSDN。
历史
- 2007 年 11 月
- 首次发布。
- 2008 年 1 月
- 添加了一个
config
属性,以禁用 Clog 对 ASP.NET Membership 的使用。 - 将 Silverlight 和 WPF 版本集成到同一个下载包中。
- 改进了多线程日志记录能力,以防止因从非 STA 线程进行日志调用而导致的异常。
- 2009 年 4 月
- 更新了文章以反映 Clog 1.8 版本的变化。请参阅 Clog 站点获取变更列表。