前回は、JMX Remoteを使用してTomcatが管理しているMBeanにアクセスし、セッション情報などを取得しました。が、このアプローチだとHttpSessionなどの情報を直接見れず、不便です。開発中の監視とかを行いたいので、やっぱりアクセスをトリガーとして動く仕組みがよいですよね。
そこで着目したのが、この記事。
http://japan.internet.com/developer/20070123/26.html
TomcatのValveを使用して、アクセス時にリソース監視を行う方法を記載しています。今回はこれに習ってリソース情報をログ出力するようなValveを書いてみましょう。
というわけで、今回はTomcatのAPIを使用します。でもって、ValveはやっぱりScalaで書きます…。
以下、用意したファイル。
build.sbt
name := "tomcat-custom-valve" version := "0.0.1-SNAPSHOT" scalaVersion := "2.9.1" organization := "littlewings" libraryDependencies ++= Seq( "org.apache.tomcat" % "tomcat-catalina" % "7.0.21" % "provided", "org.apache.tomcat" % "tomcat-coyote" % "7.0.21" % "provided" )
TomcatのAPIを使用するため、catalinaとcoyoteが入っております。
続いて、作成したValve。
MyTomcatCustomValve.scala
package littlewings import scala.collection.JavaConverters._ import java.io.IOException import java.util.Hashtable import javax.management.openmbean.CompositeData import javax.management.ObjectName import javax.servlet.ServletException import org.apache.catalina.connector.{Request, Response} import org.apache.catalina.valves.ValveBase object MyTomcatCustomValve { val DOMAIN: String = "Catalina" val HTTP_THREAD_POOL_OBJECT_NAME: ObjectName = { val hashtable = new Hashtable[String, String] hashtable.put("type", "ThreadPool") hashtable.put("name", "\"http-bio-8080\"") ObjectName.getInstance(DOMAIN, hashtable) } val MEMORY_OBJECT_NAME: ObjectName = { val hashtable = new Hashtable[String, String] hashtable.put("type", "Memory") ObjectName.getInstance("java.lang", hashtable) } } class MyTomcatCustomValve extends ValveBase { import MyTomcatCustomValve._ @throws(classOf[IOException]) @throws(classOf[ServletException]) def invoke(request: Request, response: Response): Unit = { getNext.invoke(request, response) try { monitorResource(request, response) } catch { case t: Throwable => info(request,response, "", t) } } protected def monitorResource(request: Request, response: Response): Unit = { val infoLog = info(request, response, _: Any) infoLog { request.getParameterNames.asScala.map { name => "[Name:%s, Value:%s]".format(name, request.getParameter(name)) }.mkString("HttpParameters", ", ", "") } infoLog { request.getSession(false) match { case null => "Session Not Exist" case session => session.getAttributeNames.asScala.map { name => "[Name:%s, Value:%s]".format(name, session.getAttribute(name)) }.mkString("HttpSessionAttributes", ", ", "") } } val currentThreadCount: Int = attribute(HTTP_THREAD_POOL_OBJECT_NAME, "currentThreadCount") val currentThreadsBusy: Int = attribute(HTTP_THREAD_POOL_OBJECT_NAME, "currentThreadsBusy") infoLog { "CurrentThreadCount:%d, CurrentThreadsBusy:%d".format(currentThreadCount, currentThreadsBusy) } val heapMemoryUsage: CompositeData = attribute(MEMORY_OBJECT_NAME, "HeapMemoryUsage") val nonHeapMemoryUsage: CompositeData = attribute(MEMORY_OBJECT_NAME, "NonHeapMemoryUsage") infoLog { "HeapMemory Used:%d, Max:%d".format(heapMemoryUsage.get("used"), heapMemoryUsage.get("max")) } infoLog { "NonHeapMemory Used:%d, Max:%d".format(nonHeapMemoryUsage.get("used"), nonHeapMemoryUsage.get("max")) } } protected def attribute[T](objectName: ObjectName, attributeName: String): T = mserver.getAttribute(objectName, attributeName).asInstanceOf[T] protected def info(request: Request, response: Response, message: Any, t: Throwable = null): Unit = t match { case null => containerLog.info("Context[%s] %s".format(request.getServletContext.getContextPath, message)) case _ => containerLog.info("Context[%s] %s".format(request.getServletContext.getContextPath, message, t)) } }
元のサイトに載っている通り、自作のValveを作成する際にはorg.apache.catalina.valves.ValveBaseクラスを継承して作成するのがよいみたいです。
で、ValveBaseクラスを継承したクラスで、invoke(Request, Response)メソッドをオーバーライドします。なお、ここで渡ってくるRequestとResponseは表向きの型こそorg.apache.catalina.connectorパッケージのものですが、Servlet APIのインターフェースを実装しているクラスでもあります。
つまり、Servletに渡っているRequest/Responseと同じだということですね。
なお、実体のServletとかもある意味Valveのような扱いを受けているらしく、
@throws(classOf[IOException])
@throws(classOf[ServletException])
def invoke(request: Request, response: Response): Unit = {
getNext.invoke(request, response)
でgetNext.invokeと次のValveを呼び出して実行していますが、これを忘れると実際に処理を行うServletが呼び出されなくなってしまうので、要注意です。Valveが例外でコケたりすると、そのリクエスト自体も失敗してしまうのでこの点も注意です。ちゃんとtry-catchしましょう。
なんか、Servlet Filterのノリですね。
あとは、好きな情報をログ出力とかしましょう。ValveBaseクラスを継承すると、containerLogというフィールドを利用できるようになります。これを使用してログ出力を行うと、$CATALINA_HOME/logs/localhost.yyyy-MM-dd.logファイルに結果が出力されます。ロガーの作成とかが面倒な場合は、こちらを利用するとよいでしょう。AccessLogValveのように専用のログファイルを利用したい場合は、自前で書いたりすることになります。やりたい場合は、AccessLogValveのソースを参考に。
また、ValveBaseクラスを継承すると、Tomcatが使用しているMBeanServerにアクセスすることができます。フィールド名はmserverです。これを使用すると、JMX RemoteなしでローカルアクセスのみでMBeanから情報が取得できます。
今回は、Attributeしか使用しなかったので、こんなショートカットで代替しました。
protected def attribute[T](objectName: ObjectName, attributeName: String): T = mserver.getAttribute(objectName, attributeName).asInstanceOf[T]
では、作成したアプリケーションをパッケージングして、Tomcatに配置します。こういうやつは、$CATALINA_HOME/libに置く、でいいかな。
$ pwd /xxxxx/apache-tomcat-7.0.21/lib $ ls -1 annotations-api.jar catalina-ant.jar catalina-ha.jar catalina.jar …省略… jsp-api.jar scala-library.jar servlet-api.jar tomcat-api.jar tomcat-coyote.jar tomcat-custom-valve_2.9.1-0.0.1-SNAPSHOT.jar tomcat-dbcp.jar …省略… tomcat-util.jar
今回作成したJARファイルは、「tomcat-custom-valve_2.9.1-0.0.1-SNAPSHOT.jar」という名前で配置してあります。あと、ソース自体はScalaで書いてあるので、scala-library.jarも置いています。
最後は$CATALINA_HOME/conf/server.xmlにValveの設定を追記します。
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> <!-- ここから追加分 --> <Valve className="littlewings.MyTomcatCustomValve" /> <!-- ここまで追加分 --> </Host>
AccessLogValveの次にでも書いておくといいでしょう。
では、この状態でTomcatを起動してWebアプリケーションにアクセスすると、localhost.yyyy-MM-dd.logに以下のような内容が出力されます。
※アクセスに使用したWebアプリケーションは、前回作成したリクエストパラメータをセッションに溜め込むサンプルを使用しています
Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info INFO: Context[/hello-servlet-tomcat] HttpParameters[Name:param3, Value:hello], [Name:param2, Value:bar], [Name:param1, Value:foo] Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info INFO: Context[/hello-servlet-tomcat] HttpSessionAttributes[Name:param2, Value:bar], [Name:param1, Value:foo], [Name:param3, Value:hello] Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info INFO: Context[/hello-servlet-tomcat] CurrentThreadCount:10, CurrentThreadsBusy:1 Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info INFO: Context[/hello-servlet-tomcat] HeapMemory Used:30423448, Max:924647424 Sep 19, 2011 7:02:42 PM littlewings.MyTomcatCustomValve info INFO: Context[/hello-servlet-tomcat] NonHeapMemory Used:29494352, Max:136314880
リクエストパラメータ、セッションの情報、Tomcatのスレッドプールの情報、メモリ情報が出力されていますね。
これをもうちょっと工夫すると、よい情報が取れるようになるかも…と思いたいです。
最後に、元記事にちょっと疑問なのですが…ValveクラスのインスタンスはSingletonです。なので、どのリクエストを処理するのにも同じValveクラスのインスタンスが使用されるのですが、元記事はスレッド数とかをガシガシとフィールドに格納していますが、これはあんまりよくないんじゃあ…と思う次第です。
なお、全然関係ないですが、JMX Remoteを使用していた時には簡単に取得できていたプラットフォームMBeanの情報が、ローカルで取得する場合はCompositeDataから取得しなくてはならず、これの使い方がわからなくて少々ハマりました…。