在上一篇文章《IIS日志存入數據庫之一:ODBC》中,我提到了ODBC方式保存的缺點,即:無法保存響應時間以及接收和響應的字節數。
如果一定要獲取響應時間以及接收和響應的字節數的話,就要另想辦法了。備選的方法有:
(1)尋找有沒有現成的IIS日志模塊。
(2)重寫IIS的日志模塊。
(3)在現有的IIS日志模塊的基礎上進行改造。
下面是對三種備選方法的探索:
(1)針對方法1,在IIS的官網上找到了一個名為Adanced logging的日志模塊,,,然並卵。
(2)針對方法2,改寫的工作量較大,且可以會性能問題,故拋棄。
(3)針對方法3,發現iis日志的保存目標可以為ETW事件,故采用。如下圖所示:
下面介紹一下如何訂閱IIS的ETW事件。ps:關於ETW事件的介紹,請查看我的另外一篇文章:《在.net中使用ETW事件的方法》
核心代碼
1 private void button1_Click(object sender, EventArgs e) 2 { 3 try 4 { 5 6 using (var session = new TraceEventSession("IIS-Logging")) // 創建一個session 7 { 8 session.EnableProvider("Microsoft-Windows-IIS-Logging"); // Microsoft-Windows-IIS-Logging 是IIS日志模塊提供的provider的名稱 9 10 session.Source.Registered.All += Registered_All; // 注冊事件處理函數 11 12 session.Source.Process(); // Wait for incoming events (forever). 13 } 14 } 15 catch 16 { 17 } 18 } 19 20 21 /// <summary> 22 /// 事件處理函數 23 /// </summary> 24 /// <param name="data"></param> 25 void Registered_All(TraceEvent data) 26 { 27 try 28 { 29 string logString = data.FormattedMessage; // 返回日志項的字符串形式 30 31 // 將文本轉換成對象 32 IISLogEntry logEntry = new IISLogEntry(logString); 33 34 IISLogEntry.Add(logEntry); 35 } 36 catch 37 { 38 } 39 }
上述代碼創建會話了,綁定了事件源(IIS的ETW事件提供者),訂閱了事件源。這段代碼有兩個關鍵點:
(1)怎么知道IIS日志模塊的事件提供程序的名稱是“Microsoft-Windows-IIS-Logging”呢?答案是通過命令行指令:logman query providers。下面是這個指令返回的結果:
(2)在綁定ETW事件處理程序的時候,我們使用了session.Source.Registered, session.Source.Registered返回了一個RegisteredTraceEventParser對象,通過這個對象我們才能在事件處理程序中使用data.FormattedMessage來取得日志項的內容。有關RegisteredTraceEventParser,官方文檔中有這么一句:
RegisteredTraceEventParser – which knows about any event provider that registers itself with the operating system (using the wevtutil command)).
This includes most providers that ship with the windows operating system that are NOT the kernel provider or EventSources. You can see a list of such providers with the ‘logman query providers’ command.
外圍代碼(解析日志字符串,存入數據庫)
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations; 4 using System.ComponentModel.DataAnnotations.Schema; 5 6 7 8 9 10 [Table("IISLogEntry")] 11 public class IISLogEntry 12 { 13 [Key] 14 public long Id { get; set; } 15 16 17 /// <summary> 18 /// 服務端信息 19 /// </summary> 20 public Server Server { set; get; } 21 22 /// <summary> 23 /// 客戶端信息 24 /// </summary> 25 public Client Client { get; set; } 26 27 28 /// <summary> 29 /// 請求信息 30 /// </summary> 31 public Request Request { get; set; } 32 33 34 /// <summary> 35 /// 響應信息 36 /// </summary> 37 public Response Response { get; set; } 38 39 40 41 42 /// <summary> 43 /// 構造函數 44 /// </summary> 45 public IISLogEntry() { } 46 47 48 49 /// <summary> 50 /// 構造函數。使用字符串來構造一個日志對象 51 /// </summary> 52 /// <param name="logString"></param> 53 public IISLogEntry(string logString) 54 { 55 try 56 { 57 string[] array = logString.Trim().Split(new char[] { ' ' }); 58 59 Dictionary<string, string> dictionary = new Dictionary<string, string>(); 60 //將數組中的值放入到字典中,奇數項為key,偶數項為value 61 for (int i = 0; i < array.Length; i++) 62 { 63 if (i % 2 == 0) 64 { 65 dictionary.Add(array[i], ""); 66 } 67 if (i % 2 == 1) 68 { 69 dictionary[array[i - 1]] = array[i]; 70 } 71 } 72 73 this.Server = new Server() 74 { 75 IP = dictionary["s-ip"], 76 Port = int.Parse(dictionary["s-port"]), 77 Name = dictionary["s-computername"] 78 }; 79 this.Client = new Client() 80 { 81 IP = dictionary["c-ip"], 82 UserAgent = dictionary["cs(User-Agent)"], 83 UserName = dictionary["cs-username"] 84 }; 85 this.Request = new Request() 86 { 87 RequestDateTime = DateTime.Now, 88 Method = dictionary["cs-method"], 89 UriResource = dictionary["cs-uri-stem"], 90 UriQuery = dictionary["cs-uri-query"], 91 BytesReceived = this.ConvertToLong(dictionary["cs-bytes"]) 92 }; 93 this.Response = new Response() 94 { 95 TimeTaken = this.ConvertToLong(dictionary["time-taken"]), 96 BytesSent = this.ConvertToLong(dictionary["sc-bytes"]), 97 Status = int.Parse(dictionary["sc-status"]), 98 SubStatus = int.Parse(dictionary["sc-substatus"]), 99 Win32Status = int.Parse(dictionary["sc-win32-status"]), 100 }; 101 } 102 catch (Exception exp) 103 { 104 throw new Exception("格式轉換失敗。\n日志字符串為:" + logString + "\n異常信息:" + exp.Message); 105 } 106 } 107 108 109 110 111 112 //**************************** CRUD ************************************ 113 public static bool Add(IISLogEntry data) 114 { 115 using (IISLogDbContext db = new IISLogDbContext()) 116 { 117 db.IISLogEntries.Add(data); 118 119 try 120 { 121 db.SaveChanges(); 122 return true; 123 } 124 catch 125 { 126 return false; 127 } 128 } 129 } 130 131 132 133 134 135 /// <summary> 136 /// 將帶千分號的字符串轉換成長整型。如:4,939 137 /// </summary> 138 /// <param name="str"></param> 139 /// <returns></returns> 140 private long ConvertToLong(string str) 141 { 142 string str2 = str.Replace(",", ""); 143 return long.Parse(str2); 144 } 145 146 } 147 148 149 150 151 152 /// <summary> 153 /// 服務端信息 154 /// </summary> 155 [ComplexType] 156 public class Server 157 { 158 /// <summary> 159 /// 服務器名稱。對應:s-computername 160 /// </summary> 161 [MaxLength(50)] 162 public string Name { set; get; } 163 164 165 /// <summary> 166 /// 服務器IP。對應:s-ip 167 /// </summary> 168 [MaxLength(15)] 169 public string IP { set; get; } 170 171 172 /// <summary> 173 /// 服務器端口。對應:s-port 174 /// </summary> 175 public int Port { set; get; } 176 177 178 } 179 180 181 182 /// <summary> 183 /// 客戶端信息 184 /// </summary> 185 [ComplexType] 186 public class Client 187 { 188 /// <summary> 189 /// 客戶端IP。對應:c-ip 190 /// </summary> 191 [MaxLength(15)] 192 public string IP { set; get; } 193 194 195 /// <summary> 196 /// 客戶端所使用的用戶代理。對應: cs(User-Agent) 197 /// </summary> 198 [MaxLength(200)] 199 public string UserAgent { set; get; } 200 201 202 /// <summary> 203 /// 登錄的用戶名。對應: cs-username 204 /// </summary> 205 [MaxLength(20)] 206 public string UserName { set; get; } 207 208 209 } 210 211 212 213 214 215 216 /// <summary> 217 /// 請求信息 218 /// </summary> 219 [ComplexType] 220 public class Request 221 { 222 /// <summary> 223 /// 請求時間。對應:date和time 224 /// </summary> 225 public DateTime RequestDateTime { set; get; } 226 227 228 /// <summary> 229 /// 方法。對應:cs-method 230 /// </summary> 231 [MaxLength(10)] 232 public string Method { set; get; } 233 234 235 /// <summary> 236 /// uri資源。對應:cs-uri-stem 237 /// </summary> 238 [MaxLength(1000)] 239 public string UriResource { get; set; } 240 241 242 /// <summary> 243 /// uri查詢。對應:cs-uri-query 244 /// </summary> 245 [MaxLength(1000)] 246 public string UriQuery { get; set; } 247 248 249 250 /// <summary> 251 /// 接收的字節數,單位為byte。對應:cs-bytes 252 /// </summary> 253 public long BytesReceived { get; set; } 254 255 256 257 } 258 259 260 261 /// <summary> 262 /// 響應信息 263 /// </summary> 264 [ComplexType] 265 public class Response 266 { 267 /// <summary> 268 /// 狀態碼。對應:sc-status 269 /// </summary> 270 public int Status { get; set; } 271 272 273 /// <summary> 274 /// 子狀態碼。 對應:sc-substatus 275 /// </summary> 276 public int SubStatus { get; set; } 277 278 /// <summary> 279 /// win32狀態碼。 對應:sc-win32-status 280 /// </summary> 281 public int Win32Status { get; set; } 282 283 284 /// <summary> 285 /// 發送的字節數,單位為byte。對應:sc-bytes 286 /// </summary> 287 public long BytesSent { get; set; } 288 289 290 /// <summary> 291 /// 所用時間,單位為ms。對應: time-taken 292 /// </summary> 293 public long TimeTaken { get; set; } 294 295 296 }
上述代碼是日志項實體。這里我們使用了EF作為ORM框架,mssql作為數據庫。
using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; public class IISLogDbContext : DbContext { public IISLogDbContext() : base("IISLog") { } /// <summary> /// 這個類的注釋中的值,只用於在開發的時候進行選配 /// </summary> static IISLogDbContext() { // Database.SetInitializer(new DropCreateDatabaseAlways<IISLogDbContext>()); // Database.SetInitializer(new CreateDatabaseIfNotExists<IISLogDbContext>()); // Database.SetInitializer(new DropCreateDatabaseIfModelChanges<IISLogDbContext>()); } /// <summary> /// 初始化,當模型創建的時候, /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); // 移除協定:表名復數化 base.OnModelCreating(modelBuilder); } // ********************* DbSet ************************** public DbSet<IISLogEntry> IISLogEntries { get; set; } }
上述代碼是數據上下文。
最后提一句,怎么找到官方文檔呢?在使用nuget獲取Microsoft TraceEvent Library之后,工程文件中就會多一個名為“_TraceEventProgrammersGuide.docx”的文件,它就是官方文檔。如下所示: