TextWriterTraceListener可將Trace機制輸出內容保存於檔案,便於存證、追蹤及偵錯,而Trace機制普遍應用於不少微軟產品或平台中,像是WCF Tracing,即可透過config設定將執行過程的追蹤資訊寫成檔案。

例如: 在WCF所在的web.config加入以下設定後

    </sectionGroup>
  </configSections>
  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel" 
              switchValue="Information, ActivityTracing" 
              propagateActivity="true">
        <listeners>
          <add name="traceListener" 
               type="System.Diagnostics.TextWriterTraceListener" 
               initializeData="d:\WCFTrace.txt"/>
        </listeners>
      </source>
    </sources>
  </system.diagnostics>
  <appSettings/>

可在WCFTrace.txt中找到詳細的追蹤資訊:

不過在實務應用時,會發現TextWriterTraceListener有點美中不足,最主要是TextWriterTraceListener啟用時Log檔案路徑是固定的,長久運行下來,檔案會變十分肥大(尤其追蹤資料通常很細很多),要在冗長的Log中追查特定時點發生的問題頗為困難。有個解決方式是寫個歸檔排程,在每天凌晨00:00時將現有檔案更名加上日期分檔保存,隔天起寫入原檔案,達到一天一檔的目的。但我一直在想,如果TextWriterTraceListener能自動每天獨立成一個檔案,那該有有多好?
(PS: MSDN提過一個CircularTraceListener的範例,是用兩個檔案輪流切換以控制檔案不要超過檔案上限,與理想的每日分檔有些距離)

於是動了念頭想改寫TextWriterTraceListener以實現按日分檔! 由於想用最少的Code達到目標,動用了點Hacking手法,繼承TextWriterTraceListener享受大部分的原有功能,再利用Reflection去偷改TextWriterTraceListener的私用欄位fileName,使其可以依時間變化不同的路徑及檔案名稱,就達成了每天一個檔案,甚至於每個小時獨立一個檔案的要求。

程式碼如下,有興趣的人請自行參照: (偷改父類別私用變數的Hacking行徑多為名門正派所不恥,但可以做到程式本體不用50行就KO,又能充分享受破解樂趣,對我來說實在誘人,不玩一下對不起深藏心中的駭客魂呀~~~ 衛道人士請自行迴避,不然就當成趣味惡搞一笑置之。)

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
 
namespace Darkthread
{
    /// <summary>
    /// 會依每天日期獨立個別檔案的TextWriterTraceListener
    /// </summary>
    [HostProtection(SecurityAction.LinkDemand, Synchronization = true)]
    public class DailyTextLogListener : TextWriterTraceListener
    {
        /// <summary>
        /// 入Log路徑設定,可使用{0:yyyyMMdd}依日期動態變化
        /// </summary>
        public string LogPathPattern { get; set; }
        /// <summary>
        /// 建構式,傳入Log路徑設定,可使用{0:yyyyMMdd}依日期動態變化
        /// </summary>
        /// <param name="logPathPattern"></param>
        public DailyTextLogListener(string logPathPattern)
        {
            LogPathPattern = logPathPattern;
        }
        /// <summary>
        /// 由Writer取出目前使用的Log檔路徑
        /// </summary>
        /// <returns></returns>
        private string GetCurrentFilePath()
        {
            if (Writer == null) return null;
            return ((Writer as StreamWriter).BaseStream as FileStream).Name;
        }
        /// <summary>
        /// 由當天日期及LogPathPattern設定決定當時應使用的Log檔路徑
        /// </summary>
        /// <returns></returns>
        private string GetDailyFilePath()
        {
            //未指定LogPathPattern時給予預設值
            if (string.IsNullOrEmpty(LogPathPattern))
                LogPathPattern = ".\\{0:yyyyMMdd}.log";
            return string.Format(LogPathPattern, DateTime.Now);
        }
        /// <summary>
        /// 確認目前使用的Writer有指向正確的檔案
        /// </summary>
        private void EnsureDailyLogPath()
        {
            //檢查目前的路徑是否為正確的路徑
            string dailyLogPath = GetDailyFilePath();
            if (dailyLogPath != GetCurrentFilePath())
            {
                //取出目錄部分
                string dirPath = Path.GetDirectoryName(dailyLogPath);
                //先確保路徑存在,若不存在則建立之
                if (!Directory.Exists(dirPath))
                    Directory.CreateDirectory(dirPath);
                //關閉目前使用的Log檔案
                base.Close();
                //# Hacking: 叔叔有練過 #
                //利用Reflection偷改TextWriterTraceListener的私有Field fileName
                typeof(TextWriterTraceListener)
               .GetField("fileName", BindingFlags.Instance | BindingFlags.NonPublic)
               .SetValue(this, dailyLogPath);
                //將Writer設為空值,強迫稍後改用新的檔案路徑建立FileStream
                base.Writer = null;
            }
        }
        //覆寫Write,寫入資料前確認檔案路徑
        public override void Write(string message)
        {
            EnsureDailyLogPath();
            base.Write(message);
        }
        //覆寫WriteLine,寫入資料前確認檔案路徑
        public override void WriteLine(string message)
        {
            EnsureDailyLogPath();
            base.WriteLine(message);
        }
    } 
}

有一點要注意,依MSDN文件及實測,自訂的TraceListener元件,在編譯時記得要加上數位簽署且在web.config要使用強式名稱才可運作。例如:

          <add name="traceListener" 
type="Darkthread.DailyTextLogListener, Darkthread.DailyTextLogListener, 
Version=1.0.0.0, Culture=neutral, PublicKeyToken=705dce557f423df8"
               initializeData="d:\MyLogs\{0:yyyyMM}\{0:MMdd}.log"/>

如此,就能在d:\MyLogs\201202\0211.log找到今天的追蹤記錄囉! 其實要一個小時分一個檔案也不是問題,將initializeData設成\{0:MMddHH}.log就可以了,很酷吧!


Comments

# by pH.minamo

其實,你怎麼不直接自己建立Writer就好了? 像這樣: base.Writer = new StreamWriter(dailyLogPath); 還有這種比較path的方式說真的滿粗糙的,大小寫反斜線相對位置都有可能出錯,起碼用Path.GetFullPath,或者自己暫存dailyLogPath

# by Jeffrey

to pH.minamo, 不自己建立StreamWriter的考量是在base.EnsureWriter()用了超過20行的程式在建立StreamWriter的過程,所以我選擇只把writer清空繼續延用原本的邏輯建立StreamWriter。 路徑比較的部分確實有出錯風險存在,謝謝您的建議~~

# by pH.minamo

我看了一下.NET BCL的EnsureWriter實作,我是覺得這樣的做法沒什麼問題,何況把自製的StreamWriter餵給TextWriterTraceListener是MSDN也認可的做法: http://msdn.microsoft.com/en-us/library/c5fw0173.aspx 如果真的想要用TextWriterTraceListener的邏輯,何不做個wrapper就好了,像這樣: https://gist.github.com/1806807 行數是多了幾行啦,不過少了Reflection我想在速度上應該是高了好幾倍 Reflection很好用,但是這個做法完全沒有意義,必須要先把decompiler打開觀察BCL的實作,然後大概省了不到十行的程式,我覺得這只是把打字的時間拿去觀察decompiled source而已。 而且事實上這個class假如搬到Mono上會直接爆掉,因為Mono的implementation沒有fileName這個private field。 我知道這個做法是趣味居多,所以我只是提供一些不使用Reflection的看法而已:) 我一直是這個blog的忠實讀者,希望您可以繼續寫和Reflection有關的文章。

# by Jeffrey

To pH.minamo, Wrapper的點子很棒,夠簡潔又符合正規解法(唯一的缺點是不夠有趣,哈!),謝謝提供這麼好的回饋~~ 歡迎您常來留言,能與各路高手交流是寫部落格最大的收獲!

Post a comment