在上一篇文章《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("格式转换失败。 日志字符串为:" + logString + " 异常信息:" + 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”的文件,它就是官方文档。如下所示: