ASP.NETでTraceListener使ってますか?今まで結構仕込んでおいたんだけど、ファイルやイベントログだと扱いにくいなー、なんて思ってませんか。思ってました。融通効かないなー、と。大規模サイトなんかでSQLServerに入れちゃうと、大変なことにナチャウヨ。
そこでMongoDB。みんな大好きMongoDB。ドキュメントの日本語化も着実に進んでるので、英語なんてー、と気にすることもあんまりないでしょう。そーでもないですか?いろいろ可愛いやつですよ!ログデータの保持なんて、もう、得意中の得意です。保持する構造さえちゃんとしておけば、RDBじゃ処理しにくいものもお気楽に扱えます。用途と使い方次第デスけどね。
Home - Docs-Japanese - 10gen Confluence
MongoDBって何よ?っていうのは、いろいろ検索してね。
MongoDB と NoSQL を試す
MongoDB と NoSQL を試す (第 2 部)
MongoDB と NoSQL を試す (第 3 部)
開発環境で超簡単な使い方はコンソールでmongodを起動しておいて、mongoで中身を確認。
だんだんmongodをサービス起動しておきたくなりますが、最初は単なるプロセス起動。もちろん、内容確認をコンソールのmongoで行うのも、ハッカーみたいな雰囲気あっていいかもしれないけど、そんなの面倒なので実際はGUIのツールを使いましょう。
MongoVUE | Gui tools for MongoDB
他にもイロイロあるので、気に入ったのを選んで試してみましょう。
Admin UIs – MongoDB
このMongoDBにTraceListenerからmessageを保存するようにしちゃいましょー!
開発するならHTTPでのREST操作(MongoDBに最初からあります)よりも、Driverを使った開発のほうが楽チンぽんです。前までは、いろいろ使い勝手の問題もあったりとかしたけど、いまとなっては標準公開されてるもので十分です。
CSharp Language Center – MongoDB
CSharp Driver Tutorial - MongoDB
英語かよ!どんまい。
マニュアル - Docs-Japanese - 10gen Confluence
なんにせよNuGetで取得できるのが便利なところです。
Official MongoDB C# driver - 1.2 : NuGet gallery
TraceListenerって自分で用意したことなかったんだけど、TraceListenerクラス派生でいいってことなので、お手軽ですね。必須なoverrideも2個だけ。
public override void Write(string message) { // ... } public override void WriteLine(string message) { // ... }やれそうですね。
えいやっ!
using System.Configuration; using System.Diagnostics; using MongoDB.Bson; using MongoDB.Driver; namespace MongoListener { public class MongoDbTraceListener : TraceListener { private readonly MongoServer _server; private readonly MongoDatabase _database; private readonly string _collectionName; public MongoDbTraceListener() : this("TraceData") { } public MongoDbTraceListener(string initializeData) { _collectionName = initializeData; var serverName = ConfigurationManager.AppSettings["MongoDb:Server"]; var databaseName = ConfigurationManager.AppSettings["MongoDb:Database"]; if (string.IsNullOrEmpty(serverName)) serverName = "mongodb://localhost"; if (string.IsNullOrEmpty(databaseName)) databaseName = "TraceListener"; _server = MongoServer.Create(serverName); _database = _server.GetDatabase(databaseName); } private void Insert(BsonDocument document) { var collection = _database.GetCollection(_collectionName); collection.Insert(document); } private void InternalWrite(string message) { var document = new BsonDocument {{"Message", message}}; Insert(document); } private void InternalWriteObject(object o) { var document = new BsonDocument(); var type = o.GetType(); foreach(var prop in type.GetProperties()) { document.Add(prop.Name, prop.GetValue(o,new object[]{}).ToString()); } Insert(document); } public override void Write(string message) { InternalWrite(message); } public override void WriteLine(string message) { InternalWrite(message); } public override void WriteLine(object o) { var type = o.GetType(); if (type.Name.StartsWith("<>") && type.Name.Contains("AnonymousType")) { InternalWriteObject(o); return; } InternalWrite(o.ToString()); } } }できたー。
後は、web.configに書いて使えるようにするだけですね!
<appSettings> <add key="webpages:Version" value="1.0.0.0"/> <add key="ClientValidationEnabled" value="true"/> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> <add key="MongoDb:Server" value="mongodb://localhost"/> <add key="MongoDb:Database" value="TraceListener"/> </appSettings>
<system.diagnostics> <trace autoflush="false" indentsize="4"> <listeners> <add name="mongoListener" type="MongoListner.MongoDbTraceListener, MongoListner" initializeData="MyTrace" /> <remove name="Default" /> </listeners> </trace> </system.diagnostics>
<system.web> <trace enabled="true"/>あとはSystem.Diagnostics.Trace.Write/WriteLineです!すでにたくさん仕込んでいる場合にはコレでOK。
ASP.NET MVC3標準プロジェクトを作成して、MongoDbTraceListenerクラスを作成し、HomeControllerのIndexアクションにTraceを書いてみましょう。
わーお!素敵!!
MongoDBが違うマシンだったり(本番はそうしましょう)、Database名を変更したいときはappSettingsの値を変えてね。コレクション(テーブル相当)の名前を変えたかったらtrace/listnersのinitializeDataで指定してね。
ちなみに、これだけだとつまんないので、ここから少し拡張してパフォーマンス計測してみましょう。まずはIHttpModuleを実装して、リクエストの処理時間を計測するようにしてみます。
mvc-mini-profiler - A simple but effective mini-profiler for ASP.NET and WCF - Google Project Hosting
↑パフォーマンス計測ならこんな素敵なもの(NuGit.orgで実物見れますよ~)もありますが...。これから新規ならこっちのほうが...。いや、言うまい。
どりゃ!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Web; namespace MongoListner { public class PerformanceTraceModule : IHttpModule { private string ItemKey = "_mongoDbTraceStart"; private readonly string _serverName = ""; public PerformanceTraceModule() { _serverName = Environment.MachineName; } public void Init(HttpApplication context) { context.BeginRequest += new EventHandler(context_BeginRequest); context.EndRequest += new EventHandler(context_EndRequest); } void context_BeginRequest(object sender, EventArgs e) { var httpContext = (sender as HttpApplication).Context; var startTime = httpContext.Items[ItemKey] = DateTime.Now; } void context_EndRequest(object sender, EventArgs e) { var httpContext = (sender as HttpApplication).Context; var startTime = (DateTime)httpContext.Items[ItemKey]; Trace.WriteLine(new { Server = _serverName, RequestAt = startTime, Method = httpContext.Request.HttpMethod, Status = httpContext.Response.StatusCode, RawUrl = httpContext.Request.Url.ToString(), Milliseconds = (DateTime.Now - startTime).Milliseconds }); } public void Dispose() { } } }これを利用するためにsystem.webServer/modulesに登録します。
<system.webServer> <validation validateIntegratedModeConfiguration="false"/> <modules runAllManagedModulesForAllRequests="true"> <add name="mongoListener" type="MongoListner.PerformanceTraceModule, MongoListner" preCondition="integratedMode"/> </modules> </system.webServer>実行してみます。
BeginRequestからEndRequestの間を計測するものですが490msって...。遅すぎ!
と、思ったらF5リロードの2回目は5ms。そーだろそーだろ。ん?よく見たらStatusとMillisecondsがstringになってるー。TraceListenerのInternalWriteObjectでToStringしてたね。失敬。そこは修正しましょう。書きながら作る、作りながら書く!
private void InternalWriteObject(object o) { var document = new BsonDocument(); var type = o.GetType(); foreach(var prop in type.GetProperties()) { var value = BsonValue.Create(prop.GetValue(o,new object[]{})); document.Add(prop.Name, value); } Insert(document); }これでちゃんと型どおり。BsonValueにはいろいろあるのでドキュメント参照してみてください。
このHttpModuleがあればすべてのリクエストの処理時間が計測できますね!アクセスログのTimeTakenとダダカブリ。どんまい。
IIS 6.0 ログ ファイルの Time Taken フィールドは何を表し、何を意味していますか。 : IIS 6.0 についてよく寄せられる質問
ASP.NET MVCならActionFilter属性を使ってControllerでの処理時間とViewの処理時間をそれぞれ別で計測できてなお嬉しいはず。
そいやっ!
using System; using System.Diagnostics; using System.Web.Mvc; namespace MongoListner { public class PerformanceTraceAttribute : ActionFilterAttribute { private string ItemKey = "_mongoDbFilterTraceStart"; private readonly string _serverName = ""; public PerformanceTraceAttribute() { _serverName = Environment.MachineName; } public override void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.HttpContext.Items[ItemKey] = DateTime.Now; base.OnActionExecuting(filterContext); } public override void OnActionExecuted(ActionExecutedContext filterContext) { var startTime = (DateTime)filterContext.HttpContext.Items[ItemKey]; Trace.WriteLine(new { Server = _serverName, ProcessAt = DateTime.Now, RawUrl = filterContext.HttpContext.Request.Url.ToString(), Method = filterContext.HttpContext.Request.HttpMethod, Controller = filterContext.RouteData.GetRequiredString("controller"), Action = filterContext.RouteData.GetRequiredString("action"), Milliseconds = (DateTime.Now - startTime).Milliseconds }); base.OnActionExecuted(filterContext); } public override void OnResultExecuting(ResultExecutingContext filterContext) { filterContext.HttpContext.Items[ItemKey] = DateTime.Now; base.OnResultExecuting(filterContext); } public override void OnResultExecuted(ResultExecutedContext filterContext) { var startTime = (DateTime)filterContext.HttpContext.Items[ItemKey]; var viewName = (string) null; if (filterContext.Result is ViewResult) { viewName = (filterContext.Result as ViewResult).ViewName; } Trace.WriteLine(new { Server = _serverName, ProcessAt = DateTime.Now, RawUrl = filterContext.HttpContext.Request.Url.ToString(), Result = viewName ?? filterContext.Result.GetType().Name, Controller = filterContext.RouteData.GetRequiredString("controller"), Action = filterContext.RouteData.GetRequiredString("action"), Milliseconds = (DateTime.Now - startTime).Milliseconds }); base.OnResultExecuted(filterContext); } } }これを有効にするためにGlobal Filterに追加しておきましょう。
あとは実行するだけ!
ズームして見てね。
Controllerでの実行時間が223ms、ActionResultの実行時間が180ms、HttpModuleでの計測時間が497ms。差が94msありますね。そんなもんでしょう。ちなみにコレが初回実行時の計測で、2回目は↓。
Controllerでの実行時間が0ms、ActionResultの実行時間が0ms、HttpModuleでの計測時間が10ms。差が10ms。これまた、そんなもんでしょう。
こんな感じです!楽しいですね!
途中、MongoDBに入れるDocumentのカラムを変更したりしてるけど、MongoDB側へは何も手を加える必要がないんです。ドキュメント単位(テーブルなら行単位)にカラム構成が変更されてもお構いなしです。ちなみにサーバーさえいればDatabaseもCollectionも初回アクセス時に勝手に作られるので準備は不要。この手軽さと、レスポンス性能の高さがMongoDBの魅力です。
今回のプロジェクト一式は↓こちら。ローカルにMongoDBさえ入っていればそのまま動くはず。
※ファイルを小さくするために、packages/mongocsharpdriver.1.2を消してます。