From 4a6cab3843d31f58049a5c2fc97bc77850833461 Mon Sep 17 00:00:00 2001 From: bicijinlian Date: Fri, 11 Nov 2022 13:59:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0TraceLog.Next=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Properties/launchSettings.json | 12 + .../Assets/Images/Trace.drawio | 1 + .../Assets/Images/Trace.svg | 4 + .../跟踪日志模型三要素-高级.drawio | 56 ++ .../跟踪日志模型三要素-高级.svg | 1 + .../Images/跟踪日志模型三要素.drawio | 1 + .../Images/跟踪日志模型三要素.svg | 4 + .../LogStudy.TraceLog.Next.csproj | 10 + LogStudy.TraceLog.Next/Program.cs | 82 ++ LogStudy.TraceLog.Next/学习.md | 702 ++++++++++++++++++ LogStudy.sln | 9 +- 11 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 LogStudy.EventLog/Properties/launchSettings.json create mode 100644 LogStudy.TraceLog.Next/Assets/Images/Trace.drawio create mode 100644 LogStudy.TraceLog.Next/Assets/Images/Trace.svg create mode 100644 LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.drawio create mode 100644 LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.svg create mode 100644 LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.drawio create mode 100644 LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.svg create mode 100644 LogStudy.TraceLog.Next/LogStudy.TraceLog.Next.csproj create mode 100644 LogStudy.TraceLog.Next/Program.cs create mode 100644 LogStudy.TraceLog.Next/学习.md diff --git a/LogStudy.EventLog/Properties/launchSettings.json b/LogStudy.EventLog/Properties/launchSettings.json new file mode 100644 index 0000000..5140b87 --- /dev/null +++ b/LogStudy.EventLog/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "LogStudy.EventLog": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:44107;http://localhost:44108" + } + } +} \ No newline at end of file diff --git a/LogStudy.TraceLog.Next/Assets/Images/Trace.drawio b/LogStudy.TraceLog.Next/Assets/Images/Trace.drawio new file mode 100644 index 0000000..f1bbcbd --- /dev/null +++ b/LogStudy.TraceLog.Next/Assets/Images/Trace.drawio @@ -0,0 +1 @@ +7VnLdtowEP0alsmRLWzMkkBI2qavk7SkSwUPththObJ4uF9f2ZbwA5cACeSkh3QRzdVoPNLcka/TFu5Pl1ecRP5n5gJtmchdtvCgZZoGwo78lSJJjtjtbg54PHCVUwHcBn9Ar1ToLHAhrjgKxqgIoio4ZmEIY1HBCOdsUXWbMFp9akQ8WANux4Suo6PAFX6OOmanwK8h8Hz9ZMNW+5sS7ax2EvvEZYtSVHzZwn3OmMhH02UfaHp4+lxGH5IRvXm0rz5+j5/Ij4tPd19+nuXBhrssWW2BQyj2Ds3CIchi9EZDfHdtLr6ezdGVWoLmhM7UebVMm8qHXDzIgZcONDBh8uHyFESijtZ+mjE9cRZnhe9JB9OOlvkyNV8KRKaRHIQPcZTZqAG642QM+plyN/ljq6lIuJSeWcnK5GwWupBu2pDTCz8QcBulMfFgISkuMV9MqZqOBWeP0GeU8Ww1Rsi2LUftqoRb/fRfigeUlnDT6KOeJXFCAy+UGIVJkdazJVOlnQMXsKxTS/YksCkInkgXNYtVxVQ3GpayFwW3V5hf4nVXYUS1k7eKXFBGDhRrdmCQucagrII3QSwgBH7I8qjLxtinXIeqTxtVC9Tesj72oepjneqzqT6Gbb1xgexTgTYVCKMjdhBaO21wpZpQJuPCZx4LCb0s0IuiHkhahc8NY5Gqwm8QIlGHTWaCHatGeWQte7BEZLF4cq9yzYxfqXFuaXOwLE8OEm0tA3Gv8k3HpVXSKhalhl6zG0NiNuNjeF6qCMI9UPHUuyet0UZqcaBEBPOqImziiVr6jQWZ1lGUXGlZfWd0rWqIPHe1qizBaoGwUw1kohpn882tBcpou9rPVkzWffeKws6wpLArqTop2TXlClCSN/1ZV3/ZpTYMqEivtCOpu1pfuAScybip4+yxAw8TOeNx4gZQ6bIOEBt25/Mukq7KCqvhjWQ2XHj4FS68dfV2osk7oYnhNLwYD8UTfOLJe+UJNo54n7TfQkBpcYLOu10tSXKBYnf0fLNE2VcOHVWyyYS/AQ9kLYDvJa3Kkgk3SqZt5dfBpdXK3lVatesabUtp1eOcJCW3KHWINyRsNyf8r7zq/hhX/OUgz2BfnWf9918s+svjHKFqc2OMNzf3ixtnn28S442+SZw9G6f+uqjHOVDf6Hy37ZvOi9pGmsXf6XP34n878OVf \ No newline at end of file diff --git a/LogStudy.TraceLog.Next/Assets/Images/Trace.svg b/LogStudy.TraceLog.Next/Assets/Images/Trace.svg new file mode 100644 index 0000000..006dfe8 --- /dev/null +++ b/LogStudy.TraceLog.Next/Assets/Images/Trace.svg @@ -0,0 +1,4 @@ + + + +
    Trace
    Trace
TraceListener
TraceListener
TraceListener
TraceListener
TraceListener
TraceListener
TraceFilter
TraceFilter
TraceFilter
TraceFilter
TraceFilter
TraceFilter
Text is not SVG - cannot display
\ No newline at end of file diff --git a/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.drawio b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.drawio new file mode 100644 index 0000000..b26c680 --- /dev/null +++ b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.drawio @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.svg b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.svg new file mode 100644 index 0000000..e6b6482 --- /dev/null +++ b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素-高级.svg @@ -0,0 +1 @@ +
    TraceSource
    TraceSource
TraceSwitch
TraceSwitch
TraceListener
TraceListener
TraceListener
TraceListener
TraceListener
TraceListener
TraceFilter
TraceFilter
TraceFilter
TraceFilter
TraceFilter
TraceFilter
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.drawio b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.drawio new file mode 100644 index 0000000..522bc8f --- /dev/null +++ b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.drawio @@ -0,0 +1 @@ +7VhNc5swEP01PqYDwhB8TJyvtmk7Had1elRgDWpkRIVsoL++i1kMxK7bpHHSg8cHa59WK2nfW80OA2c8Ly41T+MPKgQ5YFZYDJyzAWO25fj4VyFljXjDUQ1EWoTk1AIT8ROalYQuRAhZz9EoJY1I+2CgkgQC08O41irvu82U7O+a8gg2gEnA5SY6FaGJa9Rnxy1+BSKKm51tj+43540z3SSLeajyTlTnfOCMtVKmHs2LMcgqeU1epm/Lqby+9y7ffc5+8C+n728+fj2qg108Zsn6ChoS8+TQKrkAJONkeuHcXLH809HSuqQl1pLLBeVrwDyJm5ze4SCqBg0wU7g5ZsGUlFrvx0I1E0fZivgTdLD9tKiX0XwnEJ+nOEjusnRlW1ugG80DmKiFDqDZGe9Ub94/EMKdQ7Le2ZhWiySE6uo2TuexMDBJMTICOQodsdjMJU1nRqt7GCup9Gq1Y1me5/p0tw7ujqtfhQspOzizx9aJiziXIkoQkzBrj7UEbaB4IMc/cNkKDCsT1ByMLnEdRRkSb1STtkt23ip8Ldu4o+4RYZyKKlpHboWDA9LOI3R0vEEAhFiGZCptYhWphMvzFj1tKbLQan2ulUqJmO9gTElvCl8Y1acNM6jLW1q/Mr5Vxhu3Mc+K7uRZSdZuumk7+yn015Gbd8ZZK6BKxhP4x4TWhfD7xDdPKtcRmB1+7nY9aZDciGX/cM+uDv9V1FEIc9sZ19o4dslstVEZZVcoB0n9laS815TU6P+RFDtI6rkkxV5TUvZGL1T3IrkwQfzMDcad7w5daxuXfc5mfgBBUDUWOiC30R77CmY9aCw8d6OxsIdbGgtvX40F207KtcgMJKD32ff9S4ntiyD3Qec33Gz81s3gi/DjHvjZxc/WAnpRgrwDQbsIcqz9VRCa7deB1VznG4tz/gs= \ No newline at end of file diff --git a/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.svg b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.svg new file mode 100644 index 0000000..3c40824 --- /dev/null +++ b/LogStudy.TraceLog.Next/Assets/Images/跟踪日志模型三要素.svg @@ -0,0 +1,4 @@ + + + +
    TraceSource
    TraceSource
TraceSwitch
TraceSwitch
TraceListener
TraceListener
TraceListener
TraceListener
TraceListener
TraceListener
Text is not SVG - cannot display
\ No newline at end of file diff --git a/LogStudy.TraceLog.Next/LogStudy.TraceLog.Next.csproj b/LogStudy.TraceLog.Next/LogStudy.TraceLog.Next.csproj new file mode 100644 index 0000000..74abf5c --- /dev/null +++ b/LogStudy.TraceLog.Next/LogStudy.TraceLog.Next.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/LogStudy.TraceLog.Next/Program.cs b/LogStudy.TraceLog.Next/Program.cs new file mode 100644 index 0000000..5bf515a --- /dev/null +++ b/LogStudy.TraceLog.Next/Program.cs @@ -0,0 +1,82 @@ +using System.Diagnostics; + +namespace LogStudy.TraceLog.Next +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("跟踪日志学习-进阶项目"); + + DefaultTraceSource(); + DefaultTraceSourceWithLog(); + DefaultTraceSourceWithoutSourceLevels(); + + Console.WriteLine("按回车键,退出程序!"); + Console.ReadLine(); + } + + static void Test() + { + WeakReference s = new WeakReference(null); + } + + /// + /// 使用默认 SourceLevels 参数的 TraceSource + /// 因为 SourceLevels 默认为Off,所以使用默认不传SourceLevels参数的构建函数生成的 TraceSource 是不会输出任何跟踪信息的。 + /// + static void DefaultTraceSourceWithoutSourceLevels() + { + TraceSource defaultTraceSource = new TraceSource("Andy-DefaultTraceSourceWithoutSourceLevels"); + + var preMessaage = defaultTraceSource.Name + ": "; + + defaultTraceSource.TraceData(TraceEventType.Verbose, 1, $"{preMessaage}================"); + defaultTraceSource.TraceEvent(TraceEventType.Error, 1, $"{preMessaage}---------------"); + defaultTraceSource.TraceInformation($"{preMessaage}++++++++++++++++++++++++++++++++++++++++++++++"); + defaultTraceSource.TraceTransfer(2, $"{preMessaage}************", Guid.NewGuid()); + + //强制输出 + defaultTraceSource.Flush(); + } + + /// + /// 默认 TraceSource + /// + static void DefaultTraceSource() + { + TraceSource defaultTraceSource = new TraceSource("Andy-DefaultTraceSource", SourceLevels.All); + + var preMessaage = defaultTraceSource.Name + ": "; + + defaultTraceSource.TraceData(TraceEventType.Verbose, 1, $"{preMessaage}================"); + defaultTraceSource.TraceEvent(TraceEventType.Error, 1, $"{preMessaage}---------------"); + defaultTraceSource.TraceInformation($"{preMessaage}++++++++++++++++++++++++++++++++++++++++++++++"); + defaultTraceSource.TraceTransfer(2, $"{preMessaage}************", Guid.NewGuid()); + + //强制输出 + defaultTraceSource.Flush(); + } + + /// + /// 默认跟踪源:内容输出到日志文件 + /// + static void DefaultTraceSourceWithLog() + { + TraceSource defaultTraceSource = new TraceSource("Andy-DefaultTraceSourceWithLog", SourceLevels.All); + defaultTraceSource.Listeners.Clear(); + + defaultTraceSource.Listeners.Add(new DefaultTraceListener() { LogFileName = "DefaultTraceListener.log" }); + + var preMessaage = defaultTraceSource.Name+": "; + + defaultTraceSource.TraceData(TraceEventType.Verbose, 1, $"{ preMessaage }================"); + defaultTraceSource.TraceEvent(TraceEventType.Error, 1, $"{ preMessaage }---------------"); + defaultTraceSource.TraceInformation($"{ preMessaage }++++++++++++++++++++++++++++++++++++++++++++++"); + defaultTraceSource.TraceTransfer(2, $"{ preMessaage }************", Guid.NewGuid()); + + //强制输出 + defaultTraceSource.Flush(); + } + } +} \ No newline at end of file diff --git a/LogStudy.TraceLog.Next/学习.md b/LogStudy.TraceLog.Next/学习.md new file mode 100644 index 0000000..612f832 --- /dev/null +++ b/LogStudy.TraceLog.Next/学习.md @@ -0,0 +1,702 @@ +跟踪日志进阶 +============ + +基于 TraceSource、EventSource 和 DiagnosticSource 的日志框架都采用观察者(订阅发布)模式进行设计,日志消息由作为发布者的某个 Source 发出, +被作为订阅者的一个或多个 Listener 接收并消费。 + +在基于 TraceSource 的跟踪日志系统中,发布者和订阅者通过 TraceSource 与 TraceListener 类型来表示。跟踪日志系统还定义了一个SourceSwitch的类型。我们可以将它们称为“跟踪日志模型三要素”。 + +## 跟踪日志模型三要素 + +日志事件由某个具体的 TraceSource 发出,它们的消费实现在相应的TraceListener中,同一个 TraceSource 上可以注册多个TraceListener。 +在事件从 TraceSource 到 TraceListener 的分发过程中 SourceSwitch 起到了日志过滤的作用。 +每个 TraceSource 都拥有一个SourceSwitch,后者提供了相应的过滤策略帮助前者决定是否应该将日志消息分发给注册的 TraceListener 。 +![跟踪日志模型三要素](Assets/Images/跟踪日志模型三要素.svg) +上图展示了以 TraceSource、TraceListener 和 SourceSwitch 为核心的跟踪日志模型。 + +1. SourceSwitch + + 在介绍3个核心对象之前,我们需要先了解与跟踪日志事件类型和等级有关的两个枚举类型。 + 第一个,日志事件等级或者类型可以通过 TraceEventTvpe 枚举类型表示。如下面代码片段所示,TraceEventType 的每个枚举项都被赋予了一个值,数值越小,等级越高。 + + ```csharp + public enum TraceEventType + { + Critical = 1, + Error = 2, + Warning = 4, + Information = 8, + Verbose = 16, + + Start = 256, + Stop = 512, + Suspend = 1024, + Resume = 2048, + Transfer = 4096, + } + + ``` + + 我们可以将定义在TraceEventType中的枚举项分为两组。第1组从Critical到Verbose,它们是对某个独立事件的描述。等级最高的Critical表示致命的错误,这样的错误可能会导致程序崩溃。对于 Error 和 Warning 来说,前者表示不会影响程序继续运行的错误,后者则表示等级次之的警告。如果需要记录一些“仅供参考”之类的消息,则可以选择使用 Information类型。如果提供的消息仅供调试使用,则最好选择等级更低的Verbose。 + 第2组从 Start到Transfer,它们对应的事件关联的是某个功能性的活动(Activitv),这5种跟踪事件类型分别表示获取的5种状态变换,即开始、结束、中止、恢复和转换。 + 与跟踪事件类型或者等级相关的还包括下面的 SourceLevels 枚举。标注了 FlagsAttribute 特性的SourceLevels被TraceSource用来表示最低跟踪事件等级,它与上面介绍的TraceEventType具有密切的关系。如果给定一个具体的 SourceLevels,就能够确定定义在 TraceEventType中的哪些枚举项是与之匹配的,判定的结果由TraceEventType的值与SourceLevels的值进行“逻辑与”运算决定。对于 All 和 Off 来说,前者表示所有的跟踪事件类型都匹配,而后者的含义则与此相反。Critical、Error、Warning、Information和Verbose表示不低于指定等级的跟踪事件类型,而ActivityTracing则表示只选择5种针对活动的事件类型。 + + ```csharp + [Flags] + public enum SourceLevels + { + All=-1, + Off = 0, + Critical = 1, + Error = 2, + Warning = 4, + Information = 8, + Verbose = 16, + + ActivityTracing=65280 + } + ``` + + 了解了这两个与跟踪事件类型和等级相关的枚举类型之后,我们再来认识一下作为跟踪系统核心三要素之一的 SourceSwitch。 + 当利用 TraceSource 对象触发一个跟踪事件时,与之关联的 SourceSwitch 会利用指定的 SourceLevels 来确定该跟踪事件的类型是否满足最低等级的要求。只有在当前事件类型对应的等级不低于指定等级的情况下,跟踪事件才会被分发给注册的TraceListener对象。 + 如下面的代码片段所示,SourceSwitch派生于抽象类Switch,它表示一个般意义的“开关”,其字符串属性 DisplayName、Value和Description分别表示开关的名称、值与描述。Switch 具有一个整型的 SwitchSetting属性,用于承载具体的开关设置。当值发生改多时,它的OnValueChanged方法会作为回调被调用。 + + ```csharp + public class SourceSwitch : Switch + { + public SourceLevels Level + { + get => (SourceLevels)base.SwitchSetting; + set => base.SwitchSetting = (int)value; + } + public SourceSwitch(string name) : base(name, string.Empty) { } + + public SourceSwitch(string displayName, string defaultSwitchValue) + : base(displayName, string.Empty, defaultSwitchValue){ } + + public bool ShouldTrace(TraceEventType eventType) + { + return (SwitchSetting & (int)eventType) != 0; + } + + protected override void OnValueChanged() + { + base.SwitchSetting = (int)Enum.Parse(typeof(SourceLevels), Value, true); + } + } + + public abstract class Switch + { + public string DisplayName { get; } + public string Value { get; set; } + public string Description { get; } + + protected int SwitchSetting { get; set; } + + protected Switch(string displayName, string? description) : this(displayName, description, "0") + { + } + + protected Switch(string displayName, string? description, string defaultSwitchValue) + { + //...... + } + + protected virtual void OnSwitchSettingChanged() + { + } + + //...... + } + ``` + + 一个 SourceSwitch 对象会根据指定的 SourceLevels 确定某个跟踪事件类型是否满足设定的最等级要求。 + 但是在创建一个SourceSwitch对象时,我们并不会指定具体的 SourceLevels 枚举值是指定该枚举值的字符串表达式。 + 正因为如此,重写的OnValueChanged方法会将字符串表示等级转换成整数,并保存在 SwitchSetting 属性上。 + SourceSwitch 的 Level 属性再将该值转换成rceLevels类型。SourceSwitch 针对跟踪事件类型的过滤最终体现在它的 ShouldTrace 方法上。 + +2. TraceListener + + 作为跟踪日志事件的订阅者,TraceListener 对象最终会接收到 TraceSource 分发给它的跟踪日志消息,并对它进行进一步的处理。 + 在对 TraceListener 进行进一步介绍之前,需要先介绍几个与之相关的类型。日志消息除了承载显示消息文木,还需要包含一些与当前执行环境相关的信息,如当前进程和线程的ID、当前时间戳及调用堆线等。是否需要输出这些上下文信息,以及具体输出哪些信息是由下面的 TraceOptions 枚举类型控制的。 + 由于该枚举类型上标注了FlagsAttribute特性,所以枚举项是可以组合使用的。 + ```csharp + [Flags] + public enum TraceOptions + { + None = 0, + LogicalOperationStack = 1, + DateTime = 2, + Timestamp = 4, + ProcessId = 8, + ThreadId = 16, + Callstack = 32, + } + + ``` + + TraceOptions 枚举涉及的这些上下文信息究竟是如何收集的?这就涉及 TraceEventCache类型,作为日志内容载荷的上下文信息就保存在这个对象上。从如下所示的代码片段可以看出,定义在 TraceOptions 中除 None 外的每个枚举项在 TraceEventCache 中都有对应的属性。 + + ```csharp + public class TraceEventCache + { + private DateTime _dateTime = DateTime.MinValue; + private string _stackTrace; + private long _timeStamp= -1L; + private static volatile bool s_hasProcessId; + private static volatile int s_processId; + public string Callstack + => _stackTrace ?? (_stackTrace = Environment.StackTrace); + public DateTime DateTime + => _dateTime == DateTime.MinValue + ? _dateTime = DateTime.UtcNow + :_dateTime; + public Stack LogicalOperationStack + => Trace.CorrelationManager.LogicalOperationStack; + public int ProcessId + => GetProcessId(); + public string ThreadId + => Environment.CurrentManagedThreadId.ToString(CultureInfo.CurrentCulture); + public long Timestamp => _timeStamp == -1L + ?_timeStamp = Stopwatch.GetTimestamp() + :_timeStamp; + internal static int GetProcessId() + { + if(!s_hasProcessId) + { + s_processId = (int)GetCurrentProcessId(); + s_hasProcessId = true; + } + + return s_processId; + } + + [DllImport("kerne132.d11")] + internal static extern uint GetCurrentProcessId(); + + //...... + } + ``` + +上面介绍了与跟踪日志上下文相关的两个类型,接下来介绍另一个于它们相关的 TraceFilter 类型。 +如果 SourceSwitch 是 TraceSource 用来针对所有注册 TraceListener 的全局开关, 则 TraceFiler 是隶属于某个具体 TraceListener 的私有开关。当TraceSource将包含上下文信息的跟踪日志消息推送给 TraceListener 之后,后者会利用 TraceFilter 对其进行进一步过滤,不满足过滤条件的日志消息还是会被忽略。 +![跟踪日志模型三要素-高级](Assets/Images/跟踪日志模型三要素-高级.svg) +上图展示了引入 TraceListener 对象后跟踪日志模型的完整结构。 + +如下面的代码片段所示,抽象类 TraceFilter 将跟踪日志的过滤实现在唯一的 ShouldTrace法中。从方法的定义可以看出,用来检验是否满足过滤条件的输入包括承载当前上下文信息TraceEventCache 对象、TraceSource 的名称、跟踪日志事件类型、跟踪事件ID,以及用于格化消息内容的模板与参数列表和提供的原始数据(datal和data)。 +EventTypeFilterSourceFilter继承了这个抽象类。 + +```csharp +public abstract class TraceFilter +{ + public abstract bool ShouldTrace ( + TraceEventCache cache, + string source, + TraceEventType eventType, + int id, + string formatOrMessage, + object[] args, + object datal, + object[] data); +} +public class EventTypeFilter:TraceFilter +{ + public SourceLevels EventType { get; set;} + public EventTypeFilter(SourceLevels level) => EventType = level; + public override bool ShouldTrace + ( + TraceEventCache cache, + string source, + TraceEventType eventType, + int id, + string formatorMessage, + object[] args, + object datal, + object[] data + ) + => ((int)eventType & (int)EventType) > 0; +} + +public class SourceFilter: TraceFilter +{ + private string _source; + public string Source + { + get => _source; + set => _source = value ?? throw new AraumentNullExceptio("Source"); + } + + public SourceFilter(string source) + => _source= source?? throw new ArgumentNullException(nameof(source)); + + public override bool ShouldTrace + ( + TraceEventCache cache, + string source, + TraceEventType eventType, + int id, + string formatOrMessage, + object[] args, + object datal, + object[] data + ) => Source == source; +} +``` + +接下来将关注点重新转移到 TraceListener 上。如下面的代码片段所示,TraceListener 拍类具有一系列的属性成员,其中包括作为名称的Name、用于过滤跟踪日志的Filter,以及用控制上下文信息收集的 TraceOutputOptions。NeedIndent属性、IndentLevel属性和IndentSize性与格式化日志输出内容采用的缩进设置有关,分别表示是否需要缩进、当前缩进的等级及每次缩进的步长。 + +```csharp + public abstract class TraceListener : MarshalByRefObject, IDisposable + { + protected TraceListener(); + protected TraceListener(string? name); + + public virtual string Name { get; set; } + + public virtual bool IsThreadSafe { get; } + + public int IndentSize { get; set; } + + public int IndentLevel { get; set; } + + public TraceFilter? Filter { get; set; } + + public StringDictionary Attributes { get; } + + public TraceOptions TraceOutputOptions { get; set; } + + protected bool NeedIndent { get; set; } + + public virtual void Close(); + + public void Dispose(); + + public virtual void Fail(string? message); + + public virtual void Fail(string? message, string? detailMessage); + + public virtual void Flush(); + + public virtual void Write(object? o); + + public abstract void WriteLine(string? message); + + protected virtual void Dispose(bool disposing); + + // ...... + } +``` + +布尔类型的 IsThreadSafe 属性表示跟踪日志的处理是否线程安全,该属性默认返回 False。如果某个具体的Tacetistoner采用线程安全的方式处理跟踪日志,就应该重写该属性。 +TraceListener 还具有一个名为 Attributes 的只读属性,它利用一个 StringDictionary 对象作为容器来存储任意添加的屋性。 +TraceListener 提供了很多方法来处理 TraceSource 分发给它的跟踪日志,这些方法最终都会利用 TraceFilter 判断跟踪日志是否满足过滤条件。满足过滤规则的跟踪日志被格式化成字符串后,通过两个抽免方注(Write 和 WriteLine)输出到对应的目标渠道,所以一个派生于抽象类 TraceListener 的类型往往只需要重写这两个方法即可。 +TraceListener 提供了几组用于处理跟踪日志消息的方法,其中最核心的是用来发送跟踪事件的 3 个 TraceEvent 方法,这3个方法提供的参数包括承载上下文信息的TraceEventCache对象、TraceSource的名称、跟踪日志事件的类型和ID,以及以两种不同的方式(消息文本或者消息机板和参数)提供日志消息的内容载荷。 + +```csharp +public abstract class Tracetistener:MarshalByRefObject,IDisposable +{ + public virtual void TraceEvent(TraceEventCache eventCache, string source,TraceEventType eventType, int id); + public virtual void TraceEvent(TraceEventCache eventCache, string source,TraceEventType eventType, int id, string message); + public virtual void TraceEvent(TraceEventCache eventCache, string source,TraceEventType eventType, int id string format,params objectll args); +} + +``` + +除了上面定义的 Write 方法和 WriteLine方法,TraceListener 还定义了如下这些重载方法。据指定的内容载荷和日志类别(Category)生成格式化的消息文本后,它们会直接调用上面的抽象方法 Write或者 WriteLine 将其分发到对应的输出渠道。TraceListener的跟踪事件过于这些方法同样有效,并且进行过滤规则检验时采用的事件类型为Verbose。这些方法并不跟踪事件的ID,但是这个ID对于其他方法则是必需的。 + +```csharp +public abstract class Tracetistener:MarshalByRefObject,IDisposable +{ + public virtual void Write(object o); + public virtual void Write(object o, string category); + public virtual void Write(string message, string category); + public virtual void WriteLine(object o); + public virtual void WriteLine(object o, string category); + public virtual void WriteLine(string message, string category); +} + +``` + +下面的 TraceTransfer 方法针对的是“活动转移”跟踪事件。 +所以我们需要提供关联的活动下所示的代码片段可以看出,TraceTransfer 方法最终还是调用 TraceEvent 方法来处理消息。 +除了将跟踪事件类型设置为“Transfer”,TraceTransfer方法还会将提供的关联活后缀添加到指定的消息上。 + +```csharp +public abstract class Tracetistener:MarshalByRefObject,IDisposable +{ + public virtual void TraceTransfer(TraceEventCache? eventCache, string source, int id, string? message, Guid relatedActivityId) + { + TraceEvent + ( + eventCache, + source, + TraceEventType.Transfer, + id, + string.Create(null, stackalloc char[256], $"{message}, relatedActivityId={relatedActivityId}") + ); + } +} +``` + +除了提供一个字符串作为日志消息的内容载荷,我们还可以调用如下两个 TraceData 重载方法。它们会将一个对象或者数组作为内容载荷,TraceFitter的ShouldTrace方法最后的两个参数分别对应这两个对象。当这两个方法被调用时,它们会直接调用内容载荷对象的ToString方法将对象转换成字符串。TraceListener还定义了一些其他方法,由于篇幅有限,本节不再赘述,有兴趣的读者可以参阅相关的文档。 + +```csharp +public abstract class Tracetistener:MarshalByRefObject,IDisposable +{ + public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data); + public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data); +} +``` + +TraceListener 采用内容缓冲机制来提供内容输出的性能。对于接收到的跟踪日志,TraceListener 通常先将它们暂存在缓冲区内,累积到一定数量之后再进行批量输出。如果希望立即输出缓存区内的跟踪日志,则可以调用它的Flush方法。TraceListener类型还实现了IDisposable接口,实现的Dispose方法可以释放相应的资源。除此之外,TraceListener还定义了一个Close方法,按照约定,它与Dispose方法是等效的。一般来说,Dispose方法或者Close方法的调用会强制输出缓存区中保存的跟踪日志。 + +```csharp +public abstract class Tracetistener:MarshalByRefObject,IDisposable +{ + public virtual void Flush(); + public virtual void Close(); + public void Dispose(); +} +``` +public abstract class TraceListener:MarshalByRefObject,IDisposable + +3. TraceSource + +跟踪日志事件最初都是由某个 TraceSource 发出的,如下面的代码片段所示作为全局开关的 TraceSource 对象都有一个通过 Name 属性表示的名称。 +TraceSource 的 Switch 属性用于获取和设置作为全局开关的 SourceSwitch 对象,而 Listeners 属性则用于保存所有注册的 TraceSource 对象。 +在创建一个 TraceSource 时需要指定其名称(必需)和用于创建SourceSwitchurceLevels枚举(可选)。如果后者没有显式指定,等级会被设置为 Off,就意味着aceSource对象处于完全关闭的状态。 + +```csharp +public class TraceSource +{ + public TraceListenerCollection Listeners {get;} + public string Name {get;} + public SourceSwitch Switch {get;set;} + + public TraceSource(string name); + public TraceSource(string name, SourceLevels defaultLevel); + + //...... +} + +``` + +定义在TraceSouree类型中的所有公共方法全部标注了ConditionalAtribute特性,对应的条件编译符被设置为TRACE,如果目标程序集是在TRACE条件编译符不存在的情况下编译生成的,就意味着所有与跟踪日志相关的代码都不复存在。 +如下代码定义的 TraceEvent 重载方法是 TraceSource 最核心的3个方法,可以调用它们发送指定类型的跟踪事件。调用这些方法时除了可以指定跟踪事件类型和事件 ID,我们还可以采用两种不同的形式提供日志内容载荷。如果满足过滤规则,则这些方法最终会调用注册的TraceListener对象的同名方法完成最终的日志输出工作。 + +```csharp +public class TraceSource +{ + [Conditional("TRACE") ] + public void TraceEvent(TraceEventType eventType, int id); + [Conditional("TRACE")] + public void TraceEvent(TraceEventType eventType, int id, string message); + [Conditional("TRACE")] + public void TraceEvent(TraceEventType eventType, int id, string format, params object[] args); + + //... +} + +``` + +TraceSource 还提供了如下两个名为 TraceInformation 的重载方法,它们最终还是调用TraceEvent方法并将跟踪事件类型设置为Information。 +另一个TraceTransfer方法会发送一个类型为Transfer的跟踪事件,它最终调用的是注册TraceListener对象的同名方法。 +TraceSource具有两个名为 TraceData的方法,它们分别使用一个对象和对象数组作为跟踪日志的内容载荷,TraceSource类型同样具有对应的方法定义。 +```csharp +public class TraceSource +{ + [Conditional("TRACE")] + public void TraceInformation(string message); + [Conditional("TRACE")] + public void TraceInformation(string format, params object[] args); + [Conditional("TRACE")] + public void TraceTransfer(int id, string message, Guid relatedActivityId); + [Conditional("TRACE") ] + public void TraceData(TraceEventType eventType, int id, object data); + [Conditional("TRACE")] + public void TraceData(TraceEventType eventType, int id, params object[] data); + + //... +} + +``` + +TraceSource 最终会驱动注册的TraceListener对象来对由它发出的跟踪日志做最后的输出,这个过程可能涉及对跟踪日志的缓冲,所以可以利用 TraceSource 定义 Flush 方法驱动所有的TraceListener 来“冲洗”它们的缓冲区。 +由于 TraceListener实现了 IDispose 接口,所以TraceSource同样需要利用下面的Close方法来释放它们持有的资源。 + +```csharp +public class TraceSource +{ + public void Close(); + public void Flush(); + + //... +} + +``` + +## 预定义 TraceListener +在跟踪日志框架中,我们利用注册的 TraceListener 对象对跟踪日志消息进行持久化存储(如将格式化的日志消息保存在文件或者数据库中)或者可视化显示(如输出到控制台上),又或者将它们发送到远程服务做进一步处理。 +跟踪日志系统定义了几个原生的 TraceListener 类型。 + +1. DefaultTraceListener + +创建的 TraceSource 对象会自动注册具有如下定义的 DefaultTraceListener 对象,后者会将日志消息作为调试信息发送给调试器。DefaultTraceListener对象还可以将日志内容写入指定的文件,文件的路径可以通过LogFileName属性来指定。 + +```csharp +public class DefaultTraceListener:TraceListener +{ + public string LogFileName ( get; set; ) + public override void Write(string message); + public override void WriteLine(string message); + + // ...... +} + + +``` + +我们通过一个简单的程序来演示 DefaultTraceListener 针对文件的日志输出。 +如下面的代码片段所示,在创建一个 TraceSource对象之后,我们将默认注册的TraceListener清除,并注册了根据指定的日志文件(trace.log)创建的DefaultTraceListener对象,针对每种事件类型输出一条日志。 + +```csharp + using System.Diagnostics; + + var source = new TraceSource("Foobar", SourceLevels.A11); + source.Listeners.Clear(); + source.Listeners.Add(new DefaultTraceListener(LogFileName ="trace.log"); + var eventTypes =(TraceEventTypell)Enum.GetValues(typeof(TraceEventType)); + var eventId =1; + Array.ForEach(eventTypes, + it => source.TraceEvent (it, eventId++, $"This is a (it) message.")); + +``` + +运行程序后,我们会发现编译输出目录下会生成一个trace.log文件,程序中生成的10条跟踪日志会逐条写入该文件中。 +DefaultTraceListener对象在进行文件日志输出时,只将格式化的日志消息以追加的形式写入指定的文本文件中。 + +2. TextWriterTraceListener + +由于跟踪日志的内容载荷最终都会格式化成一个字符串,字符串的输出可以由 TextWriter 对象来完成。 +一个 TextWriterTraceListener 对象利用封装的 TextWriter 对象完成跟踪志内容的输出工作。 +如下面的代码片段所示,这个 TextWriter 对象体现 TextWriterTraceListener 的 Writer 属性上。 + +```csharp +public class TextNriterTraceListener :TraceListener +{ + public TextWriter Writer ( get;set;) + public TextWriterTraceListener(); + public TextWriterTraceListener(Stream stream); + public TextWriterTraceListener(TextWriter writer); + public TextWriterTraceListener(Stream stream, string name); + public TextWriterTraceListener(TextWriter writer, string name); + public TextWriterTraceListener(string fileName); + public TextWriterTraceListener(string fileName, string name); + public override void Write(string message); + public override void WriteLine(string message); + public override void Close(); + protected override void Dispose(bool disposing); + public override void Flush(); +} +``` + +TextWriterTraceListener 有3个派生类,分别是 ConsoleTraceListener、DelimitedListTraceListener 和 XmlWriterTraceListener。 +根据类型命名可以看出,它们将日志消息分别写入控制台、分隔符列表文件和XML文件。 + +3. DelimitedListTraceListener + +DelimitedListTraceListener 是 TextWriterTraceListener的子类,它在对跟踪日志信息讲行格式化时采用指定的分隔符。 +如下面的代码片段,Delimiter属性代表的就是这个分隔符,在默认情况下采用分号(;)作为分隔符。 + +```csharp +public class DelimitedListTraceListener :TextWriterTraceListener +{ + public string Delimiter ( get;set;) + public DelimitedListTraceListener(Stream stream); + public DelimitedListTraceListener(TextWriter writer); + public DelimitedListTraceListener(string fileName); + public DelimitedListTraceListener(Stream stream, string name); + public DelimitedListTraceListener(TextWriter writer, string name); + public DelimitedListTraceListener(string fileName, string name); + public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id,object data); + public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params objectl] data); + public override void TraceEvent(TraceEventCache eventCache, string source TraceEventType eventType, int id, string message); + public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params objectl] args); +} + +``` + +基于分隔符的格式化实现在重写的 TraceData 方法和 TraceEvent 方法中,所以调用 TraceSource 对象的 Write 方法或者 WriteLine 方法时输出的内容不会采用分隔符进行分隔。 +对第二个TraceData重载方法,如果传入的内容载荷对象是一个数组,那么每个元素之间同样会用分隔符进行分隔。 +在默认情况下,采用的分隔符为逗号,但是如果 Delimiter 属性表示主分隔符为逗号,此分隔符就会选择分号(;)。 +如下所示的代码片段展示了在选用默认分隔的情况下分别通过TraceData方法和TraceEvent方法输出的文本格式。 + +```txt + TraceData 1: + (SourceName);(EventType]; (EventId); (Data); (ProcessId); (LogicalOperationStack); (Thread + d);(DateTime);(Timestamp); + TraceData 2: + (SourceName); (EventType): (EventId); (Datal), (Data2)..(DataN]; (ProcessId);(LogicalOperatio Stack);(ThreadId); (DateTime);(Timestamp); + TraceEvent + (SourceName);(EventType); (EventId);(Message);; (ProcessId); (LogicalOperationStack); (ThreadId); (DateTime);(Timestamp); + +``` + +上面展示的跟踪日志输出格式中的占位符“LogicalOperationStack)”表示当前逻辑操作调用堆栈。 +上述代码片段还揭示了另一个细节,那就是对TraceEvent方法的输出格式来说,表示日志消息主体内容的“Message)”和表示进程ID的“Processld”之间会出现两个分符,这可能是一个漏洞(Bug)。 +如果采用逗号()作为分隔符,那么最终输出的是一个CS(Comma Separated Value)文件。 +在如下所示的实例中,首先将当前目录下一个名为trace.csv 的文件作为日志文件,然后据这个文件的 FileStream 创建了一个 DelimitedListTraceListener 对象并将其注册到TraceSource对象上,最后针对每种事件类型输出了10条日志。 + +```csharp + using System.Diagnostics; + + var fileName ="trace.csv"; + File.AppendAllText (fiieName,@$"gourceNane, EventType, EventId,Message,N/A, ProcessId, LogicaloperationStack, Threadtd.Daterime, Timestamp,( Environment.NewLine)"); + using (var filestream= new Filestream(fileName, FileMode.Append)) + { + TraceOptions options = Traceoptiong.Callstack | TraceOptions.DateTime TraceOptions.Logicaloperationstack | TraceOptions.ProcessId | + TraceOptions.Threadid | TraceOptions.Timestamp; + + var listener = new DelimitedListTracelistener(fileStream)(TraceOutputOptions = options, Delimiter=",”); + var source = new TraceSource("Foobar", SourceLevels.A11) ; + source.Listeners,Add(listener); + + var eventTypes = (TraceEventTypell)Enum.GetValues(typeof(TraceEventType)) ;for (int index = 0; index < eventTypes.Length; index++) + var enventType = eventTypes[index]; + var eventId = index + 1; + Trace.CorrelationManager.StartLogicalOperation($"Op(eventId)"); + source.TraceEvent(enventType, eventId.$"This is a (enventType) message."); + } + + source.Flush(); + +``` + +为了演示上面提到的逻辑操作的调用堆栈,我们利用 Trace 类型得到一个 CorrelationManager对象,并调用其 StartLogicalOperation 方法启动一个以“Op(eventld”格式名的逻辑操作。 +由于 DelimitedListTraceListener对象内部采用了缓冲机制,所以调用 TaceSource 对象的 Flush 方法强制输出缓冲区中的跟踪日志。 +程序运行之后输出的10条跟踪日将全部记录在trace.csv 文件中,如果直接利用Excel打开这个文件,就会看到日志内容容。 + +## Trace + +除了创建一个 TraceSource 对象来记录跟踪日志,还可以直接使用 Trace 类型来完成类似工作。 +Trace 是一个本应定义成静态类型的实例类型,它的所有成员都是静态的。我们可以 Trace 类型视为一个单例的 TraceSource 对象,TraceListener 对象以全局的形式直接注册在这 Trace 类型之上。 +与 TraceSource 不同的是,Trace 并不存在一个 SourceSwitch 作为全局开关对发出的跟踪件进行过滤,所以调用 Trace 相应方法发出的跟踪事件会直接分发给注册的所有TraceListener》象。 +TraceListener 对象可以通过自身的 TraceFilter 对接收的跟踪事件进行过滤,下图展现静态类型Trace的跟踪事件分发处理机制。 +![Trace跟踪](Assets/Images/Trace.svg) + + +与 TraceSource 一样,Trace 的所有公共方法全部标注了 ConditionalAttribute特性,并将牛编译符设置为“TRACE”。 +如下面的代码片段所示,全局注册的 TraceListener 对象保存在过静态属性 Listeners 表示的集合中。 +Trace类型的三组方法会发送类型分别为 Error、Information 和 Warning 的跟踪事件,这些方法最终调用的是所有注册 TraceListener 对象的 TraceEvent 方法。 + +```csharp +public sealed class Trace +{ + public static TraceListenerCollection Listeners ( get;) + [Conditional("TRACE")] + public static void TraceError(string message); + [Conditional("TRACE") ] + public static void TraceError(string format, params objectl] args); + [Conditional("TRACE")] + public static void TraceInformation(string message); + [Conditional("TRACE")] + public static void TraceInformation(string format, params objectl] args); + [Conditional("TRACE")] + public static void TraceWarning(string message); + [Conditional("TRACE")] + public static void TraceWarning(string format, params objectl] args); + + //...... +} + +``` + +Trace 同样定义了如下所示的一系列 Write 方法和 WriteLine 方法,以及携带前置条件的 Writelf 方法和 WritelineIf 方法、这些方法最终都会调用注册的 TraceListener 对象的 Write 方法和 Writeline方法完成对日志的输出。 + +```csharp +public sealed class Trace +{ + [Conditional("TRACE")] + pubiie statie vold rite(object valve) + [Conditional("TRACE")] + publie static void Wtite(string mesbage) ) + [Conditional("TRACE")] + pubiic static void Writerobject valve, string category); + [Conditional("TRACE")] + pubiie statie void Writeistring message, string category); + [Conditional("TRACE")] + public statie void Writelf(bool condition, object value); + [Conditional("TRACE")] + pubiic static void Writelf(bool condition, string message); + [Conditional("TRACE")] + pubiic static void Writelf(bool condition, object value, string category); + [Conditional("TRACE")] + public static void Writelf(bool condition, string message, string category); + [Conditional("TRACE")] + public static void WriteLine(object value); + [Conditional("TRACE")] + public static void WriteLine(string message); + [Conditional("TRACE")] + public static void WriteLine(object value, string category); + [Conditional("TRACE")] + public static void WriteLine(string message, string category); + [Conditional("TRACE")] + public static void WriteLineIf(bool condition, object value); + [Conditional("TRACE")] + public static void WriteLineIf(bool condition, string message); + [Conditional("TRACE")] + public static void WriteLineIf(bool condition, object value, string category); + [Conditional("TRACE")] + public static void WriteLineIf(bool condition, string message, string category); + + //...... +} + +``` + +为了使输出的日志的内容更具结构化和可读性,我们可以利用定义在Trace类型中如下与缩进相关的成员。 +IndentSize属性表示一次缩进的字符数,默认值为4,而IndentLevel属用于返回或者设置当前的缩进层级。 +除了直接使用IndentLevel属性来控制缩进层级,还可!用Indent方法和Unindent方法以递增或者递减的形式来设置缩进层级。 + +```csharp +public sealed class Trace +{ + public static int Indentievel ( get; set; ) + public static int Indentsize ( geti set; ) + [Conditional("TRACE")] + public static void Indent(); + [Conditional("TRACE")] + public static void Unindent(); + + //...... +} + +``` + +Trace 类型中同样定义了与日志输出缓冲机制相关的成员。Flush 方法用来强制输出存储在TraceListener 中的跟踪日志。 +如果不希望跟踪日志在 TraceListener 的缓存区停留,则可以将AutoFlush属性设置为True,Flush方法会在接收到分发的跟踪事件后被调用。 + + +```csharp +public sealed class Trace +{ + public static bool AutoFlush ( get; set;) + public static CorrelationManager CorrelationManager f get; ) + [Conditional("TRACE")] + public static void Close(); + [Conditional("TRACE")] + public static void Flush(); + + //...... +} + +``` + +除了这两个与缓冲输出有关的成员,Trace类型还定义了Close方法,它会调用TraceListener对象的同名方法来完成对资源的释放。 +它还有一个名为CorrelationManager的属性,它返回的CorrelationManager对象可以开启和关闭一个逻辑操作,已经在上面的演示实例中使用过这个对象。 + + + + + + diff --git a/LogStudy.sln b/LogStudy.sln index 1d524b4..3f8382e 100644 --- a/LogStudy.sln +++ b/LogStudy.sln @@ -24,7 +24,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogStudy.EventLog", "LogStu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogStudy.DiagnosticLog", "LogStudy.DiagnosticLog\LogStudy.DiagnosticLog.csproj", "{73AC3F84-DC11-4F53-A729-0EA09E124E6E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogStudy.EventLog.Next", "LogStudy.EventLog.Next\LogStudy.EventLog.Next.csproj", "{D77E4B1E-D839-446B-AB96-46CC5E07D36F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogStudy.EventLog.Next", "LogStudy.EventLog.Next\LogStudy.EventLog.Next.csproj", "{D77E4B1E-D839-446B-AB96-46CC5E07D36F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogStudy.TraceLog.Next", "LogStudy.TraceLog.Next\LogStudy.TraceLog.Next.csproj", "{5BC961D8-3DC8-4A18-A787-AA475865C801}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -56,6 +58,10 @@ Global {D77E4B1E-D839-446B-AB96-46CC5E07D36F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D77E4B1E-D839-446B-AB96-46CC5E07D36F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D77E4B1E-D839-446B-AB96-46CC5E07D36F}.Release|Any CPU.Build.0 = Release|Any CPU + {5BC961D8-3DC8-4A18-A787-AA475865C801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BC961D8-3DC8-4A18-A787-AA475865C801}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BC961D8-3DC8-4A18-A787-AA475865C801}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BC961D8-3DC8-4A18-A787-AA475865C801}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -66,6 +72,7 @@ Global {372EA389-4C91-4B05-8270-86788D48F45D} = {78E9B0D7-537A-4791-BE80-E2BA04D8A5AA} {73AC3F84-DC11-4F53-A729-0EA09E124E6E} = {D91AB187-F922-47DE-87ED-D3785F4E8D7B} {D77E4B1E-D839-446B-AB96-46CC5E07D36F} = {78E9B0D7-537A-4791-BE80-E2BA04D8A5AA} + {5BC961D8-3DC8-4A18-A787-AA475865C801} = {353B9475-1064-473D-B3EE-CF3A9BBA4102} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4D9A3A36-98D0-498C-B78A-2E5CFC5195AC}